JPA(Java Persistence API)

자바 진영의 ORM 기술 표준

 

ORM?

- Object-relational mapping(객체 관계 매핑)

- 객체는 객체대로 설계

- 관계형 데이터베이스는 관계형 데이터베이스대로 설계

- ORM 프레임워크가 중간에서 매핑

- 대중적인 언어에는 대부분 ORM 기술이 존재한다.

 

JPA는 애플리케이션과 JDBC 사이에서 동작한다.

개발자가 직접 JDBC API를 쓰는게 아니라 개발자가 JPA를 사용하면 JPA 내부에서 JDBC API를 사용해 DB와 통신한다.

 

JPA 저장 과정

예를 들어 MemberDAO에서 데이터를 저장하고 싶다면

  1. JPA에게 Member 객체를 넘긴다.
  2. JPA가 Member 객체를 분석한다.
  3. JPA가 Insert Query를 사용
  4. JPA가 JDBC API를 사용하여 DB에 보낸다.

 

JPA 조회 과정

MemberDAO에서 멤버를 찾고싶다면

  1. JPA에게 멤버의 ID값을 넘긴다.
  2. JPA가 ID값으로 SQL query를 만든다.
  3. JDBC API 통해 DB에 보내고 결과를 받는다.
  4. 결과를 가지고 Member Object에 매핑을 해준다.

 

JPA는 표준 명세이다. 내부 코드는 거의 인터페이스로 되어있다. JPA 표준 명세를 구현한 3가지가 있는데 하이버네이트, EclipseLink, DataNucleus가 있다.

 

JPA를 왜 사용해야 할까?

  1. SQL 중심적인 개발에서 객체 중심으로 개발
  2. 생산성
  3. 유지보수
  4. 패러다임의 불일치 해결
  5. 성능
  6. 데이터 접근 추상화와 벤더 독립성
  7. 표준

생산성 - CRUD 만들기가 매우 편하다.

  • 저장 : jpa.persist(member)
  • 조회 : Member member = jpa.find(memberId)
  • 수정 : member.setName("변경할 이름")
  • 삭제 : jpa.remove(member)

유지보수 - SQL을 사용하면 필드가 추가, 제거되면 모든 SQL을 수정 해야한다. 그러나 JPA를 사용한다면 필드만 추가하면 JPA가 알아서 해준다.


성능 

  • 1차 캐시와 동일성 보장

  • 트랜잭션을 지원하는 쓰기지연 - Insert
    • 트랜잭션을 커밋할 때까지 INSERT SQL을 모아둔다.

 


하이버 네이트란?

하이버네이트는 자바 환경에서의 ORM이다.

ORM은 객체와 데이터베이스 테이블간에 데이터 타입이나 관계를 자동으로 매핑시켜 해결해준다.

 


Spring Data JPA란?

JPA를 쓰기 편하게 만들어 놓은 모듈로 개발자가 JPA를 더 쉽고 편하게 사용할 수 있도록 도와준다. 

Repository라는 인터페이스를 사용하여 정해진 규칙대로 메소드를 입력하면, Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어 Bean으로 등록해준다.

 


하이버네이트와 Spring Data JPA의 차이점

하이버네이트는 JPA의 구현체이고 Spring Data JPA는 JPA에 대한 데이터 접근의 추상화라고 할 수 있다.

Spring Data JPA는 항상 하이버네이트와 같은 JPA 구현체가 필요하다.

 

참고 : 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 (inflearn.com)

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

JPA란? - spring (gitbook.io)

 

JPA란?

 

dahye-jeong.gitbook.io

 

전체 코드는 여기에서 볼 수 있습니다.

오늘은 게시물에 댓글 기능을 적용해보도록 하겠습니다.

- Controller, Service, Entity, Repository, DTO

- CommentController

@RestController
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;

    @PostMapping("/post/{id}/comment")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "댓글 쓰기", notes = "id에 해당하는 게시글에 댓글을 작성합니다.")
    public SuccessResponse<String> createComment(@PathVariable(name = "id") Long postId,
                                                 @Valid @RequestBody CommentDTO commentDTO,
                                                 Principal principal) {

        CommentDTO comment = commentService.createComment(postId, commentDTO, principal.getName());

        return SuccessResponse.success(null);

    }
}

- CommentService

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

    private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    @Transactional
    public CommentDTO createComment(Long postId, CommentDTO commentDTO, String email) {
        Optional<Post> byId = postRepository.findById(postId);

        Post post = byId.orElseThrow(() -> new PostNotFound("게시물이 삭제되었거나 존재하지 않습니다."));

        Optional<User> byEmail = userRepository.findByEmail(email);

        User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다."));

        Comment comment = Comment.builder()
                .contents(commentDTO.getContents())
                .build();

        comment.mappingPostAndUser(post, user);

        Comment saveComment = commentRepository.save(comment);

        return CommentDTO.convertToCommentDto(saveComment);
    }
}

- CommentEntity

@Entity
@Getter
@Builder
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
public class Comment {

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

    @Column(nullable = false, length = 100)
    private String contents;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "post_id", foreignKey = @ForeignKey(name = "FK_user_comment"))
    private Post post;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "FK_post_comment"))
    private User user;

    public void mappingPostAndUser(Post post, User user) {
        this.post = post;
        this.user = user;

        post.mappingComment(this);
        user.mappingComment(this);
    }
}

- CommentRepository

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}

- CommentDTO

@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Data
public class CommentDTO {
    private Long id;

    @NotBlank
    @Length(max = 100)
    private String contents;
    private UserDTO user;

    public static CommentDTO convertToCommentDto(Comment comment) {
        return CommentDTO.builder()
                .id(comment.getId())
                .contents(comment.getContents())
                .user(UserDTO.convertToUserDTO(comment.getUser()))
                .build();
    }

    public static List<CommentDTO> convertToCommentDtoList(List<Comment> commentList) {
        Stream<Comment> stream = commentList.stream();

        return stream.map(CommentDTO::convertToCommentDto).collect(Collectors.toList());

    }
}

 

여기 까지가 Comment 관련 코드이고 이제 Comment와 Post, User와 관계를 지정해주어야 합니다.

Comment와 Post는 N : 1 관계, Comment와 User는 N : 1 관계 입니다. 그러므로 PostEntity와 UserEntity에 연관관계및 연관관계 메서드를 설정해줍니다.

- PostEntity

    @OneToMany(fetch = LAZY, mappedBy = "post")
    private List<Comment> commentList = new ArrayList<>();
    
        public void mappingComment(Comment comment) {
        this.commentList.add(comment);
    }

- UserEntity

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Comment> commentList = new ArrayList<>();
    
    public void mappingComment(Comment comment) {
        commentList.add(comment);
    }

 

그리고 이제 게시물에 댓글을 달고 게시물을 조회하고 나면 쿼리가 무언가 이상합니다. 

1번 게시물에 1번 유저, 2번 유저, 3번 유저가 댓글을 달경우 1번 게시물을 조회할때 1, 2, 3번 유저를 조회하는 쿼리가 나가기 때문입니다.

이를 해결하기 위해 저번에 작업한 PostRepository에 최적화 작업을 하겠습니다.

- PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"category", "user", "commentList.user"})
    @Override
    Optional<Post> findById(Long postId);

    @EntityGraph(attributePaths = {"user", "category"})
    @Override
    List<Post> findAll(Sort sort);
}

@EntityGraph에 "commentList.user"를 추가해줌으로써 CommentList와 여기에 댓글을 작성한 유저의 정보까지 한번에 가져오므로 쿼리 1번에 모두 조회가 가능합니다.

전체 코드는 여기에서 보실 수 있습니다.

먼저 코드를 보여드리도록 하겠습니다.

Controller, Service, PostEntity

- PostController

@PostMapping("/post")
    @ResponseStatus(value = HttpStatus.CREATED)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 생성", notes = "게시물을 생성합니다.")
    public SuccessResponse<PostDTO> createPost(@Valid @RequestBody PostDTO postDTO, Principal principal) {
        log.info(principal.getName());
        PostDTO post = postService.createPost(postDTO, principal.getName());

        return SuccessResponse.success(post);
    }

 

이전 Json으로 로그인하기를 했을때 UserController의 login 메서드에서 email, password를 받아 UsernamePasswordAuthenticationToken을 만들어주고 이것을 UserAuthenticationProvider에 넘겨주었습니다. Provider에서 넘겨준 토큰으로 email과 password를 꺼내 DB에서 유저 정보를 조회해 로그인이 성공하면 UsernamePasswordAuthenticationToken으로 Authentication을 리턴하였습니다. 그로인해 로그인이 Principal를 통해 현재 로그인한 유저의 정보를 가져올 수 있습니다.

- PostService

public PostDTO createPost(PostDTO postDTO, String email) {
        Optional<User> byEmail = userRepository.findByEmail(email);

        User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("게시글 작성 권한이 없습니다."));

        Post post = Post.builder()
                .title(postDTO.getTitle())
                .contents(postDTO.getContents())
                .createAt(LocalDateTime.now())
                .build();

        post.mappingCategory(postCategoryRepository.findByName(postDTO.getCategory()));
        post.mappingUser(user);

        Post savePost = postRepository.save(post);

        return PostDTO.builder()
                .id(savePost.getId())
                .build();
    }

받은 email로 DB에서 유저를 찾은 다음에 post에 매핑시켜 줍니다.

- UserEntity, PostEntity

- UserEntity

@Entity
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String password;

    private String nickname;

    private LocalDateTime createdBy;

    @Builder.Default
    private String role = "ROLE_USER";

    private String authority;

    private boolean enabled = true;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Post> postList = new ArrayList<>();

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public void encryptPassword(String password) {
        this.password = BCrypt.hashpw(password, BCrypt.gensalt());
    }

    public void mappingPost(Post post) {
        postList.add(post);
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> auth = new HashSet<>();
        auth.add(new SimpleGrantedAuthority(authority));

        return auth;
    }

    @Override
    public String getUsername() {
        return this.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }


}

- PostEntity

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String contents;

    private LocalDateTime createAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_category_id")
    private PostCategory category;

    @Builder.Default
    private Long viewCount = 0L;

    public void addViewCount() {
        this.viewCount += 1;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

////////////////////////////////////////////////////////////////////////////////////////////////

    public void changeTitle(String title) {
        this.title = title;
    }

    public void changeContents(String contents) {
        this.contents = contents;
    }

    public void mappingCategory(PostCategory postCategory) {
        this.category = postCategory;
        postCategory.mappingPost(this);
    }

    public void mappingUser(User user) {
        this.user = user;
        user.mappingPost(this);
    }
}

유저는 포스트를 여러개 작성 할수 있고 포스트는 유저 1명에게 속해 있어야하니 유저 : 포스트 는 1 : N 관계 입니다. 그러므로 User Entity에는 @ManyToOne 으로 매핑 시켜 주고, 포스트에는 @OneToMany로 매핑 시켜줍니다. N인 Post가 매핑관계의 주인이 되는게 좋으므로 mappedBy를 통해 Post의 user 필드를 지정해 주고 Post Entity에 유저를 매핑시켜줄 연관관계 메서드를 작성해줍니다.

여기 까지 진행한 후 게시물을 만들고 getPost를 통해 게시물을 조회하게 되면 쿼리가 2개가 발생하게 됩니다. Post를 가져올때 한번, Post에 매핑된 User를 가져올때 한번 일어나게 되는데요 이것을 쿼리 한번에 조회하기 위해 PostRepository를 수정해줍니다.

PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"category", "user"})
    Optional<Post> findById(Long postId);
}

지난시간 postCategory 쿼리를 최적화 하기위해 @EntityGraph에 category를 지정해줬는데 user를 추가시켜주어 쿼리 1번에 유저까지 가져올수 있도록 합니다.

그 후 게시물 조회를 해보면 유저까지 한번에 가져오는걸 알 수 있습니다.

전체 코드는 여기에서 확인할 수 있습니다.

 

안녕하세요 이번에는 API를 테스트 할 수 있는 Swagger 문서를 추가 해보도록 하겠습니다.

build.gradle 파일에 추가해줍니다.

// swagger
    compile('io.springfox:springfox-swagger2:2.7.0')
    compile('io.springfox:springfox-swagger-ui:2.7.0')

그 다음 SwaggerConfig 파일을 만들어 줍니다.

@Configuration
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Spring Boot REST Sample with Swagger")
                .description("API 문서")
                .termsOfServiceUrl("")
                .contact(new Contact("user", "url", "user@example.com"))
                .version("1.0")
                .build();

    }
}

그 다음으로 Controller에 코드를 추가 해줍니다. 저는 PostController에 추가 해주도록 하겠습니다.

@RestController
@RequiredArgsConstructor
@Api(tags = {"게시물 관련 API"})
public class PostController {
    private final PostService postService;

    @GetMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @ApiOperation(value = "게시물 조회", notes = "게시물을 조회합니다.")
    public SuccessResponse<PostDTO> getPost(@PathVariable(name = "id") Long id) {
        PostDTO post = postService.getPost(id);

        return SuccessResponse.success(post);
    }

    @PostMapping("/post")
    @ResponseStatus(value = HttpStatus.CREATED)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 생성", notes = "게시물을 생성합니다.")
    public SuccessResponse<PostDTO> createPost(@Valid @RequestBody PostDTO postDTO) {
        PostDTO post = postService.createPost(postDTO);

        return SuccessResponse.success(post);
    }

    @PutMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 수정", notes = "게시물을 수정합니다.")
    public SuccessResponse<PostDTO> updatePost(@Valid @RequestBody PostDTO postDTO, @PathVariable(name = "id") Long postId) {
        PostDTO postDTOResponse = postService.updatePost(postId, postDTO);

        return SuccessResponse.success(postDTOResponse);
    }

    @DeleteMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 삭제", notes = "게시물을 삭제합니다.")
    public SuccessResponse<String> deletePost(@PathVariable(name = "id") Long postId) {
        postService.deletePost(postId);
        return SuccessResponse.success(null);
    }

    @GetMapping("/post-category")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 카테고리 조회", notes = "게시물 카테고리를 조회합니다.")
    public SuccessResponse<List<PostCategoryDTO>> postCategory() {
        List<PostCategoryDTO> postCategoryDTOS = postService.postCategoryList();

        return SuccessResponse.success(postCategoryDTOS);
    }
}

@Api : 해당 컨트롤러가 어떤 API 그룹인지 적어주세요.

@ApiOperation : value -> api의 제목? 을 적어주세요.

@ApiOperation : notes -> api에 대한 설명을 적어주세요.

그 후 Main 실행 파일에 가셔서 @EnableSwagger2 를 붙여주세요.

@SpringBootApplication
@EnableSwagger2
public class BoardRestApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardRestApiApplication.class, args);
    }

}

 

저 같은 경우에는 Spring Security를 사용하고 있기 때문에 swagger 관련 주소를 제외시켜주어야 합니다. SecurityConfig에 추가시켜줍니다.

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/webjars/**")
                .antMatchers("/v2/api-docs/**")
                .antMatchers("/configuration/**")
                .antMatchers("/swagger*/**");
    }

 

이제 실행하신다음 http://localhost:8080/swagger-ui.html 에 접속하시면 아래와 같은 화면이 나오며 api를 테스트 할 수 있습니다.

왜 테스트 코드를 작성해야 하나요?

테스트 코드를 작성을 왜 하는지 궁금하신 분들이 계실겁니다.

물론 테스트 코드를 작성하지 않고 API를 만든 후에 포스트맨같은 툴로 API를 테스트 할 수도 있을겁니다. 저 역시 이전에는 그렇게 하였구요. 그러나 이 방법의 최대 단점은 손이 많이 간다는 것입니다.

예를 들어 회원가입을 한다면 확인해야 할 것이 아래와 같습니다.

  1. Email이 중복이 아닌가?
  2. 비밀번호가 양식에 맞는가?
  3. 필수로 입력해야할 칸이 비어있지는 않는가?
  4. 가입이 성공적으로 완료되었을때 응답을 올바르게 오는가?
  5. 1,2,3번에 해당하였을 경우 에러를 잘 뱉어내는가?

위와 같은 테스트 케이스들이 많습니다! 무려 회원가입 API 하나만 체크하는데도 최소 5개가 되는 케이스를 테스트 해야합니다.

이걸 포스트맨으로 일일히 옮겨 적는다면.. 예전 생각이 나서 정말 끔찍하네요.

이러한 노가다를 방지하기 위해 테스트 코드를 작성하는데요? 물론 테스트코드 역시 위와 같은 케이스를 모두 작성해주어야 하는 번거로움은 있습니다. 그러나 테스트코드를 작성하면 위와같은 케이스를 한번 클릭으로 모두 테스트할 수 있기때문에 시간 절약이 됩니다!

 

Controller Test

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class PostControllerTest {
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private PostService postService;

    public PostDTO initPostDTO() {
        return PostDTO.builder()
                .title("title")
                .contents("contents")
                .build();
    }

    @Test
    @DisplayName("게시물 등록 테스트")
    @Order(1)
    public void createPost() throws Exception {
        // given
        PostDTO postDTO = initPostDTO();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("data").exists())
                .andExpect(jsonPath("data").hasJsonPath())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("message").value("success"))
                .andExpect(jsonPath("data.id").value(1))
                .andExpect(status().isCreated())
                .andDo(print());
    }

    @Test
    @DisplayName("게시물 가져오기 테스트 - 성공")
    @Order(2)
    public void getPostSuccess() throws Exception {
        // given
        PostDTO postDTO = initPostDTO();
        postService.createPost(postDTO);

        // when
        ResultActions perform = mockMvc.perform(get("/post/{id}", 1));

        // then
        perform
                .andExpect(status().isOk())
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("success"))
                .andExpect(jsonPath("data").exists())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("data.title").exists())
                .andExpect(jsonPath("data.title").value("title"))
                .andExpect(jsonPath("data.contents").exists())
                .andExpect(jsonPath("data.contents").value("contents"))
                .andDo(print());
    }

    @Test
    @DisplayName("존재하지않는 게시물 가져오기")
    @Order(3)
    public void getPostFail() throws Exception {
        mockMvc.perform(get("/post/{id}", 9999999))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("error"))
                .andExpect(jsonPath("errors[0]").exists())
                .andExpect(jsonPath("errors[0].field").doesNotExist())
                .andExpect(jsonPath("errors[0].message").exists())
                .andExpect(jsonPath("errors[0].message").value("해당 포스트가 존재하지 않습니다."))
                .andDo(print());
    }

    @Test
    public void 게시물_제목_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .contents("contents")
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("title")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("제목을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 게시물_내용_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .title("title")
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("contents")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("내용을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 게시물_내용_제목_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(2)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("title", "contents")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("제목을 입력해주세요.", "내용을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

}

 

 

errors[*] 문법은 errors의 원소의 순서없이 containsInAnyOrder의 값이 있냐없냐를 체크하기 위함입니다.

진행 코드는 여기서 볼 수 있습니다.

+ Recent posts