no image
[JPA] 더티 체킹(Dirty Checking)
더티 체킹(Dirty Checking)은 트랜잭션 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터 베이스에 반영하는 JPA의 특징 중 하나이다. 우선 데이터베이스에 변경 데이터를 저장하는 시점은 Transaction Commit, EntityManager flush, JPQL 사용 이렇게 3가지가 있다. JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터 베이스에 자동으로 반영해준다.(변화의 기준은 최초 조회 상태) JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅숏을 만든다. 트랜잭션이 끝나는 시점에 스냅숏과 현재의 상태를 비교해 다른 점이 있다면 Update 쿼리를 데이터 베이스로 날린다. 이제 예시를 보며 더 자세하게 보자 public voi..
2021.12.22
no image
[JPA] N + 1 이슈
JPA를 사용하다 보면 가장 자주 마주하게 되는 이슈가 N+1이다. N+1이 발생할 때 데이터의 양이 아주 적으면 그냥 무심코 넘길 수 있지만 데이터가 많아질수록 성능에서 엄청난 차이를 가져온다. 이번 포스팅에서는 문제가 발생하는 예시와 해결 방안에 대해서 정리하려 한다. Post를 호출하여 Comment를 사용할 것이다라고 가정하자. ➡️엔티티 클래스 post @Entity @Getter @NoArgsConstructor public class Post { @Id@GeneratedValue @Column(name = "post_id") private Long id; private String name; @OneToMany(mappedBy = "post",cascade = CascadeType.ALL,f..
2021.12.17
no image
[JPA] 양방향 연관관계(무한 루프)
🧑🏻‍💻양방향 연관관계(무한 루프) JPA로 개발을 하다 보면 양방향으로 매핑하여 참조가 필요한 모델들이 종종 있다. 이러한 과정에서 양방향 관계를 가진 객체를 직렬화 하려고 할 때 무한으로 참조가 되어 StackOverFlow를 발생시킨다. 그러나 이러한 문제가 있다고 JPA가 가진 장점을 포기하고 단방향으로 구성하기엔 너무 비효율적이다. 이를 해결하려면 양방향 연관관계에서 연관관계의 주인을 설정해주어야 한다. 예시 엔티티 @Entity public class User { @Id @GeneratedValue private Long id; private String name; private int age; @ManyToOne private Team team; } @Entity public class Te..
2021.12.14
no image
[Querydsl] Projection 정리
🧭Querydsl Projection Projection은 테이블의 특정한 칼럼들만 따로 조회한다는것을 의미한다. 결과값을 반환할 때, DTO로 반환이 가능하다. 이때 DTO클래스로 반환라기 위해 Projection 하는 방법이 여러가지 있다. Tuple Projection.bean Projection.constructor Projection.field @QueryProjection 📡Tuple을 이용한 Projection void ProjectionByTuple()throws Exception{ Listtuples = jpaQueryFactory .select(member.name, member.age) .from(member) .fetch(); } Tuple을 통해서 반환은 하나의 데이터로 반환한다...
2021.12.12
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 3
🛠Controller 마지막으로 Api 컨트롤러를 구현할 것이다. RestController를 사용할 것이다. 전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용한다. @RestController는 Spring MVC Controlle에 @ResponseBody가 추가된 것이다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것이다. ➡️SaveCategory @PostMapping public ResponseEntity saveCategory(@Valid@RequestBody SaveRequest request){ categoryService.createCategory(request); return OK; } 우선..
2021.12.10
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 2
이번 포스팅은 Service를 구현하고 그에 대한 테스트 코드를 작성할 것이다. 우선 Service를 구현하기 전에 계층 간 데이터 전송을 하기 위해 먼저 DTO를 구현한다. 🧷DTO @Getter @NoArgsConstructor public static class SaveRequest{ private String categoryName; private String parentCategory; private Map subCategory; @Builder public SaveRequest(String categoryName, String parentCategory, Map subCategory) { this.categoryName = categoryName; this.parentCategory = par..
2021.12.10
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 1
토이 프로젝트를 진행하면서 카테고리를 만드는데 무한 뎁스로 만드는 과정을 기록하려 한다. Stack : Java,Gradle, Spring data jpa ,Mysql 위 사진 처럼 유연하게 설계하여 무한하게 카테고리가 생성이 가능한 로직을 구현할것이다. 🔓요구조건 Entity 구현 하나의 테이블에서 구현해야하기 떄문에 Self Join 사용하여 구현 Repository 인터페이스 구현 Spring data JPA를 사용을 위해 JpaRepository를 상속 받는다. DTO 구현 Entity는 DB를 생성하고 DB를 가지고 계층간의 소통을 하기 위해 DTO를 구현할 것이다. Entity에서 list로 담은 childCategory는 DTO에서는 Map으로 구현한다. Service 구현 Save : 카테..
2021.12.10
no image
[TestCode] AssertJ에 대하여(JUnit5)
💻AssertJ란 AssertJ는 많은 assertion을 제공하는 자바 라이브러리이다. 오류 메세지와 테스트 코드의 가독성을 높이고 자신이 사용하는 IDE에서 사용하기에 용이하다. AssertJ는 사이드 프로젝트로 만들어졌다. 만약 무엇인가 잘못된것이 있다면 언제든지 피드백을 할 수 있다고 원작자가 말하였다. 가독성 이야기를 해보자면 //JUnit assertEquals(expected, actual); //AssertJ assertThat(actual).isEqualTo(expected); 두 예제가 있는데 첫번째가 JUnit에서 제공하는 assertEquals이다. 아래는 AssertJ의 AssertThat이다. AssertThat이 확실히 직관적으로 보이고 가독성이 올라간다. 가장 기본적인 예제 ..
2021.12.07
728x90

더티 체킹(Dirty Checking)은 트랜잭션 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터 베이스에 반영하는 JPA의 특징 중 하나이다.

  • 우선 데이터베이스에 변경 데이터를 저장하는 시점은 Transaction Commit, EntityManager flush, JPQL 사용 이렇게 3가지가 있다.
  • JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터 베이스에 자동으로 반영해준다.(변화의 기준은 최초 조회 상태)
  • JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅숏을 만든다. 트랜잭션이 끝나는 시점에 스냅숏과 현재의 상태를 비교해 다른 점이 있다면 Update 쿼리를 데이터 베이스로 날린다.

이제 예시를 보며 더 자세하게 보자

 public void updateNative(Long id, String trade){
        EntityManager em = entityManagerFactory.createEntityManager();
        EntityTransaction ef = em.getTransaction();
        ef.begin();
        Payment payment = em.find(Payment.class, id);
        payment.changeTradeNumber(trade);
        ef.commit();
    }

코드를 보면 별도로 데이터베이스에 저장하지 않는다.

트랜잭션을 시작되고 엔티티 조회 후 엔티티값을 변경하고 트랜잭션을 커밋한다.

@Test
@DisplayName("엔티티 메니저로 확인")
void find_entityManager()throws Exception{
    //given
    Payment pay = paymentRepository.save(new Payment("test1",  100L));

    //when
    String updateTradeNo = "test2";
    paymentService.updateNative(pay.getId(), updateTradeNo);

    //then
    Payment saved = paymentRepository.findAll().get(0);
    Assertions.assertThat(saved.getTradeNumber()).isEqualTo(updateTradeNo);

}

위 테스트를 수행하면, 아래와 같은 로그를 확인할 수 있다.

Hibernate: 
    update 
    	payment 
    set 
        amount=?, 
        trade_number=? 
    where 
        id=?

save 메서드로 변경 사항을 저장하지 않았지만 update 쿼리가 실행된다.

이것이 더티체킹의 역할이다.

위에 소개에서 말한 것처럼 JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.

여기서 JPA는 엔티티 조회를 하면 엔티티의 조회 상태를 스냅숏으로 만들어 놓고 트랜잭션이 끝나는 시점에는 조회 상태를 저장한 스냅숏과 비교해서 다른 점이 있으면 Update 쿼리를 데이터베이스로 전달한다.

여기서 더티 체킹(상태 변경 검사)의 대상은 영속성 콘텍스트가 관리하는 엔티티에만 적용된다.

데이터베이스에 반영되기 전 처음으로 생성된 엔티티나 detach 된 엔티티는 더티 체킹 대상에 들어가지 않는다.(값을 변경해도 데이터 베이스에 반영되지 않는다.)

 

 

이제 Spring Data Jpa와 @Transactional을 같이 사용하는 경우를 보자

@Transactional
public void update(Long id,String tradeNumber){
    Payment payment = paymentRepository.getOne(id);
    payment.changeTradeNumber(tradeNumber);
}
@Test
@DisplayName("SpringDataJpa로_확인")
void find_jpa() {
    //given
    Payment pay = paymentRepository.save(new Payment("test1",  100L));
    //when
    String updateTradeNo = "test2";
    paymentService.update(pay.getId(), updateTradeNo);
    //then
    Payment saved = paymentRepository.findAll().get(0);
    Assertions.assertThat(saved.getTradeNumber()).isEqualTo(updateTradeNo);
}
Hibernate: 
    update 
    	payment 
    set 
        amount=?, 
        trade_number=? 
    where 
        id=?

정상적으로 쿼리가 실행된다.

 

현재는 JPA에서 전체 필드를 업데이트하는 방식을 기본값으로 사용한다.

전체 필드를 업데이트를 하는 방식은 생성되는 쿼리가 같아 부트 실행 시점에 미리 만들어서 재사용이 가능하고, 데이터베이스 입장에서도 재사용이 가능하다.

다만, 필드가 많아질 경우 전체 필드에 대한 Update 쿼리는 부담스러울 수 있다.

이럴 경우 @DynamicUpdate로 변경 필드만 반영되도록 할 수 있다.

Hibernate:
        update 
             payment 
        set 
             trade_number=? 
        where 
              id=?

테스트 코드를 다시 실행해보면 변경한 trade_number만 update 쿼리에 반영된 것을 확인할 수 있다.

 

 

 

🧑🏻‍💻예제 코드: https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com


REFERENCE

https://jojoldu.tistory.com/415

728x90

[JPA] N + 1 이슈

ryudjae
|2021. 12. 17. 17:04
728x90

JPA를 사용하다 보면 가장 자주 마주하게 되는 이슈가 N+1이다. N+1이 발생할 때 데이터의 양이 아주 적으면 그냥 무심코 넘길 수 있지만 데이터가 많아질수록 성능에서 엄청난 차이를 가져온다.

이번 포스팅에서는 문제가 발생하는 예시와 해결 방안에 대해서 정리하려 한다.

Post를 호출하여 Comment를 사용할 것이다라고 가정하자.


➡️엔티티 클래스

post

@Entity
@Getter
@NoArgsConstructor
public class Post {

    @Id@GeneratedValue
    @Column(name = "post_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "post",cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();

    @Builder
    public Post(String name, List<Comment> comments) {
        this.name = name;
        if(comments != null){
            this.comments = comments;
        }
    }

    public void addComment(Comment comment){
        this.comments.add(comment);
        comment.updatePost(this);
    }
}

comment

@Entity
@Getter
@NoArgsConstructor
public class Comment {

    @Id@GeneratedValue
    private Long id;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

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

    public void updatePost(Post post){
        this.post = post;
    }
}

Repository

@Repository
public interface PostRepository extends JpaRepository<Post,Long> {
}

Service

@RequiredArgsConstructor
@Slf4j
@Service
public class PostService {

    private final PostRepository postRepository;

    @Transactional(readOnly = true)
    public List<String> findAllComments() {
        return extractSubjectNames(postRepository.findAll());
    }

    private List<String> extractSubjectNames(List<Post> posts) {
        log.info("Comments Size : {}", posts.size());

        return posts.stream()
                .map(a -> a.getComments().get(0).getContent())
                .collect(Collectors.toList());
    }
}

ServiceTest

 

@SpringBootTest
class PostServiceTest {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private PostService postService;


    @BeforeEach
    public void setUp(){
        List<Post>posts = new ArrayList<>();

        for(int i = 0;  i< 10; i++){
            Post post = Post.builder()
                    .name("게시글"+i)
                    .build();
            post.addComment(Comment.builder()
                            .content("댓글"+i)
                            .build());
            posts.add(post);
        }
        postRepository.saveAll(posts);
    }


    @Test
    @DisplayName("게시글 조회시 댓글 조회 N+1 발생")
    void N_plus_1_O()throws Exception{
        List<String> comments = postService.findAllComments();
        assertThat(comments.size()).isEqualTo(10);

    }
}

 

위 테스트 코드를 실행하면 아래처럼 Post를 조회하는 쿼리와 Post의 자신의 Comment를 조회하는 쿼리가 10번이 나간다.

 

이게 오늘 정리하는 N+1 쿼리이다.

위처럼 하위 엔티티를 한 번에 가져오지 않고 지연 로딩으로 필요한 곳에서 사용되어 쿼리가 실행될 때 발생하는 문제이다.

지금은 Post가 10개이니 처음 조회되는 쿼리 한 번과 10번의 Comment조회가 발생하여 총 11번의 쿼리가 발생한다.

지금은 11번만 나가지만 조회 결과가 1만 개면 DB 조회가 최소 1만 번은 일어나서 성능이 급격하게 저하된다.

이제 이러한 문제에 대한 해결책을 보자.

 

1.fetch join

@Query("select a from Post a join fetch a.comments")
List<Post>findAllFetchJoin();

fetch join은 바로 가져오고 싶은 Entity 필드를 지정하는 것이다.

@Test
@DisplayName("fetchjoin으로 게시글 가져옴")
void fetchJoin_getPost()throws Exception{
    List<Post> post = postRepository.findAllFetchJoin();
    assertThat(post.size()).isEqualTo(10);
      
}

테스트 코드를 실행하면 쿼리가 한 줄만 실행된다.

이로써 문제는 해결되지만 불필요한 쿼리문이 추가된다는 단점이 존재한다.

필드마다 Eager.Lazy 방식을 다르게 해서 쿼리가 불필요하면 @EntityGraph를 사용하면 된다.

 

2.EntityGraph

@EntityGraph의 attributePaths에 쿼리 수행 시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager로 조회가 된다.

@EntityGraph(attributePaths = "comments")
@Query("select a from Post a")
List<Post> findAllEntityGraph();

위처럼 원본 쿼리의 손상 없이 (Eager/Lazy) 필드를 정의하고 사용할 수 있게 된다.

테스트 코드를 작성해보자 

@Test
@DisplayName("게시글 여러개를 EntityGraph 로 가져온다.")
void entityGraph()throws Exception{

    List<Post> post = postRepository.findAllEntityGraph();
    assertThat(post.size()).isEqualTo(10);

}

 

테스트 코드 결과를 보면 정상적으로 조회 쿼리가 작동한다.

 

 

두 방식 모두 카테시안 곱이 발생하여 comment의 수만큼 Post가 중복 발생하게 된다.

FetchJoin은 InnerJoin이고, @EntityGraph는 OuterJoin이다. 

➡️fetchJoin

select 
      post0_.post_id as post_id1_3_0_, 
      comments1_.id as id1_1_1_, 
      post0_.name as name2_3_0_, 
      comments1_.content as content2_1_1_, 
      comments1_.post_id as post_id3_1_1_, 
      comments1_.post_id as post_id3_1_0__, 
      comments1_.id as id1_1_0__ 
from post post0_ 
     inner join comment comments1_ 
             on post0_.post_id=comments1_.post_id

➡️@EntityGraph

select post0_.post_id as post_id1_3_0_, 
       comments1_.id as id1_1_1_, 
       post0_.name as name2_3_0_, 
       comments1_.content as content2_1_1_, 
       comments1_.post_id as post_id3_1_1_, 
       comments1_.post_id as post_id3_1_0__, 
       comments1_.id as id1_1_0__ 
from post post0_ left 
     outer join comment comments1_ 
             on post0_.post_id=comments1_.post_id

위 쿼리문은  위 두 방식의 조회 쿼리이다. 이제 중복을 확인해보자

@BeforeEach
public void setUp(){
   List<Post>posts = new ArrayList<>();

   for(int i = 0;  i< 10; i++){
         Post post = Post.builder()
               .name("게시글"+i)
                .build();
          post.addComment(Comment.builder(
                 .content("댓글"+i)
                 .build());
          post.addComment(Comment.builder()
                .content("댓글1"+i)
                .build());
            posts.add(post);
    }
    postRepository.saveAll(posts);
}

@Test
@DisplayName("Posts여러개를_joinFetch로_가져온다")
void postFetchJoin() throws Exception {
    //given
    List<Post> posts = postRepository.findAllFetchJoin();
    List<String> comments = postService.findAllSubjectNamesByJoinFetch();
    
    //then
    assertThat(posts.size()).isEqualTo(20);
    assertThat(comments.size()).isEqualTo(20);
    System.out.println("post size : " + posts.size());
    System.out.println("comments size : " + comments.size());
    }

테스트 코드를 실행해보면 Post의 개수가 10개가 아닌 20개로 생성된다.

 

이 문제의 해결법을 해결하는 데는 2가지의 방법이 있다.

첫 번째는 @OneToMany 필드를 Set 선언해주면 된다. Set은 중복을 허용하지 않아서 하나의 해결 방법이 된다.

@OneToMany(mappedBy = "post",cascade = CascadeType.ALL,fetch = FetchType.LAZY)
private Set<Comment> comments = new LinkedHashSet<>();

Set은 순서도 보장하지 않기 때문에 LinkedHashSet을 사용하면 순서도 보장이 된다.

 

그러나 List를 써야 하는 상황이라면 distinct을 사용하여 중복을 제거해주는 방식이다.

@Query("select DISTINCT a from Post a join fetch a.comments")
List<Post> findAllJoinFetchDistinct();
@Test
@DisplayName("distinct_FetchJoin")
void distinct()throws Exception{
    //given
    List<Post> posts = postRepository.findAllJoinFetchDistinct();
    //then
    assertThat(posts.size()).isEqualTo(10);
    System.out.println("post size : " + posts.size());
  }

중복되지 않고 정상적으로 조회가 된다.

@EntityGraph(attributePaths = "comments")
@Query("select DISTINCT a from Post a")
List<Post> findAllEntityGraphDistinct();
@Test
@DisplayName("distinct_EntityGraph")
void distinct_EntityGraph()throws Exception{
    //given
    List<Post> posts = postRepository.findAllEntityGraphDistinct();
    //then
    assertThat(posts.size()).isEqualTo(10);
    System.out.println("post size : " + posts.size());
}

또한 정상적으로 작동한다.

 

두 방식 중 자신의 프로젝트 상황에 맞게 사용하면 된다.

 

 

🧑🏻‍💻예제 코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com


Reference

https://jojoldu.tistory.com/165?category=637935

728x90
728x90

🧑🏻‍💻양방향 연관관계(무한 루프)

JPA로 개발을 하다 보면 양방향으로 매핑하여 참조가 필요한 모델들이 종종 있다. 이러한 과정에서 양방향 관계를 가진 객체를 직렬화 하려고 할 때 무한으로 참조가 되어 StackOverFlow를 발생시킨다. 그러나 이러한 문제가 있다고 JPA가 가진 장점을 포기하고 단방향으로 구성하기엔 너무 비효율적이다.

이를 해결하려면  양방향 연관관계에서 연관관계의 주인을 설정해주어야 한다.


예시 엔티티

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;

    @ManyToOne
    private Team team;
}
@Entity
public class Team {

    @Id@GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany
    private List<User> members = new ArrayList<>();
}

예시로 위 두개의 엔티티가 있다.

User와 Team은 ManyToOne의 관계이고 Team과 User는 OneToMany이다.

OneToMany의 관계의 경우 여러 건의 연관관계를 맺기 위해 Collection을 사용하여야 한다.

OneToMany@OneToMany 어노테이션을 붙여주면 되고 ManyToOne@ManyToOne 붙여주면된다.

이제 양방향으로 되었다면 연관관계 주인을 정해줘야 한다.

만약 연관관계 주인을 설정을 안 해준다면 위 예시로 들면 User를 조회하면 Team이 조회가 된다. 그리고 Team에서 User를 조회한다.

이런 식으로 계속 서로를 참조하여 무한루프에 갇힌다.

무한루프에 갇히면서 제일 처음 말했던 StackOverFlow가 발생한다.

이러한 문제를 연관관계 주인을 정해줌으로써 해결할 수 있다.

 

🧑🏻‍💻연관관계 주인 설정

일반적으로 양방향 관계를 맺으면 연관관계 주인을 주로 외래 키가 저장되는 측을 연관관계의 주인으로 지정한다.

외래키가 저장되는 엔티티에서는 @JoinColumn으로 저장할 외래키가 있는 엔티티의 키를 name속성으로 외래 키를 지정해주면 된다

@Entity
public class User {
	.
	.
    .
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

이런 식으로 외래키 속성을 지정해주고 주인이 아닌 엔티티에는 mappedBy를 통해 주인이 설정한 필드명을 통해 연관관계 주인을 설정한다.

@Entity
public class Team {
	.
	.
    .
    @OneToMany(mappedBy = "team")
    private List<User> Users = new ArrayList<>();
}

주인을 설정하기 위해 User 엔티티에 외래키로 설정된 필드명을  가져와서 주인을 지정해준다. 여기선 User 엔티티의 team이 외래키로 설정된 필드이기 때문에 mappedBy = "team"으로 설정을 해준다.

@ManyToOne은 항상 외래키가 저장되어 연관관계의 주인이 되기 때문에 mappedBy 속성을 설정할 수도 없다. 

 

🧑🏻‍💻양방향 매핑 정리

  • 테이블 연관관계의 주인은 두 테이블 중 외래키를 관리하는 쪽을 주인이라 한다.(보통 외래 키가 저장되는 테이블)
  • 연관관계 주인은 mappedBy속성을 사용하지 않는다. 주인이 아닐 경우에만 mappedBy로 연관관계 주인을 지정해주어야 한다.
  • 연관관계 주인만이 DB 연관관계와 매핑되고, 외래키를 관리할 수 있다.
  • @ManyToOne은 항상 연관관계 주인이다.

🧑🏻‍💻예제 코드 : https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

728x90

'Dev > SpringData' 카테고리의 다른 글

[JPA] 더티 체킹(Dirty Checking)  (0) 2021.12.22
[JPA] N + 1 이슈  (0) 2021.12.17
[Querydsl] Projection 정리  (0) 2021.12.12
[Querydsl] QueryDSL 사용법 정리  (0) 2021.12.03
[Spring] DTO 사용범위에 대하여  (0) 2021.11.12
728x90

🧭Querydsl Projection

Projection은 테이블의 특정한 칼럼들만 따로 조회한다는것을 의미한다. 결과값을 반환할 때, DTO로 반환이 가능하다.

이때 DTO클래스로 반환라기 위해 Projection 하는 방법이 여러가지 있다. 

  • Tuple
  • Projection.bean
  • Projection.constructor
  • Projection.field
  • @QueryProjection

📡Tuple을 이용한 Projection

void ProjectionByTuple()throws Exception{
    List<Tuple>tuples = jpaQueryFactory
    		.select(member.name, member.age)
            .from(member)
            .fetch();

        
}

 

  • Tuple을 통해서 반환은 하나의 데이터로 반환한다.
  • Tuple을 사용하여 조회하는 방법은 권장하지 않는다.Tuple 은 com.querydsl.core import 를 물고 있기 때문에 이후에 querydsl 을 쓰지 않도록 서비스가 바뀌게되면 유연한 대처를 하지 못하기 때문이다.

📡Projection.Bean

//DTO
@Setter
@NoArgsConstructor
public static class MemberDtoByBean{
   private String name;
   private int age;
}

void ProjectionByBean()throws Exception{
    List<MemberDto.MemberDtoByBean> memberDto = jpaQueryFactory
            .select(Projections.bean(MemberDto.MemberDtoByBean.class,member.name,member.age))
            .from(member)
            .fetch();

   memberDto.forEach(System.out::println);
}
  • Projection.Bean()이용하여 Setter를 통해서 조회한다.

 

📡Projection.constructor

//DTO
public static class MemberDtoByConstructor{
    private String name;
    private int age;

    public MemberDtoByConstructor(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

void ProjectionByConstructor()throws Exception{
    List<MemberDto.MemberDtoByConstructor> memberDto = jpaQueryFactory
          .select(Projections.constructor(MemberDto.MemberDtoByConstructor.class, member.name, member.age))
          .from(member)
          .fetch();
}
  • DTO에 생성자를 미리 만들고 Projection.constructor를 이용하여 조회할 수 있다.
  • 여기서 조회하는값은 DTO 생성자에 무조건 들어있어야 한다.

 

📡Projection.field

//DTO
@NoArgsConstructor
public static class MemberDtoByField{
     private String name;
     private int age;
}

void ProjectionByField()throws Exception{
      List<MemberDto.MemberDtoByField> memberDto = jpaQueryFactory
          .select(Projections.fields(MemberDto.MemberDtoByField.class, member.name, member.age))
          .from(member)
          .fetch();
}
  • Projection.field를 이용할 때는 DTO를 만들고 필드값을 삽입한다.
  • 접근 지정자가 private라고 하더라도 값의 삽입이 가능하다.(projection 내부에 리플렉션 API를 사용하고 있기 때문에 접근이 가능하다.)

 

📡@QueryProjection

//DTO
@Getter
public static class MemberDtoByQueryProjection{
    private String name;
    private int age;

    @QueryProjection
    public MemberDtoByQueryProjection(String name, int age) {
         this.name = name;
         this.age = age;
    }

}

void ProjectionByQueryProjection()throws Exception{
   List<MemberDto.MemberDtoByQueryProjection> memberDto = jpaQueryFactory
          .select(new QMemberDto_MemberDtoByQueryProjection( member.name, member.age))
          .from(member)
          .fetch();
}
  • DTO클래스 생성자에 @QueryProjection을 어노테이션을 달아 놓고 조회하는 방법이 있다.
  • compileQuerydsl을 수행시켜주면 Q-Class에 Dto클래스가 생성된다.

무엇을 쓰던지 상관없다.

Constructor을 통한 Projection조회 시에는 .select()절에 새로운 컬럼을 추가해도 컴파일 에러가 발생하기 때문에 런타임과정에서 발생하기 때문에 개발자가 사전에 인지하지 못할 수도 있다.그러나 @QueryProjection은 새로운 칼럼 추가시, 컴파일에러가 발생해서 사전에 알아차리기 쉽다.

setter나 field는 새로운 칼럼 추가하더라도 Projection 유형이 setter,field이기 때문에 조회하여도 무방하다.

 

🧑🏻‍💻예제 코드 : https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com


Reference

 

728x90
728x90

🛠Controller

  • 마지막으로 Api 컨트롤러를 구현할 것이다.
  • RestController를 사용할 것이다.
    • 전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용한다.
    • @RestController는 Spring MVC Controlle에 @ResponseBody가 추가된 것이다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것이다.

 

➡️SaveCategory

 

@PostMapping
public ResponseEntity<Void> saveCategory(@Valid@RequestBody SaveRequest request){
    categoryService.createCategory(request);
    return OK;
}

 

우선 부모 카테고리를 넣으면 정상적으로 값이 들어간다.

이후 자식 카테고리를 넣으면 정상적으로 부모 밑으로 값이 들어가는 것을 확인할 수 있다.

 

 

➡️getCategoeyById

@GetMapping("/{id}")
 public ResponseEntity<CategoryResponse> getCategoryById(@PathVariable(name = "id") Long categoryId){
    return categoryService.getCategoryById(categoryId);
}

 

이제 조회를 해보자 . 위에서 만든 카테고리를 조회해보자. (부모와 자식을 모두 조회할 것이다.)

부모를 조회하면 자식까지 정상적으로 조회되는 것을 볼 수 있다.

 

➡️updateCateogory

@PatchMapping("/{id}")
 public ResponseEntity<Void> updateCategory(@PathVariable(name = "id") Long id,
                               @Valid@RequestBody SaveRequest request){
    categoryService.updateCategory(id, request);
}

 

이제 수정을 해볼 것이다.

 

정상적으로 수정이 되었다. 

 

➡️deleteCategory

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCategory(@PathVariable(name = "id")Long id){
    categoryService.deleteCategory(id);
}

 

이제 삭제를 해보자

삭제는 자식 카테고리가 있으면 삭제가 불가능하도록 로직이 구현되어 있다.

만약 자식 카테고리가 있다면 예외가 정상적으로 던져진다.

 

이제 정상적인 삭제를 해보자

요청을 하면 쿼리가 나가는지 보자

상적으로 쿼리가 나가고 DB에서 값이 삭제되었다.

 

 

 


이로써 카테고리 로직 구현은 마무리가 되었다. 사실 구현하기 전에는 어려움이 없을 것이라고 생각했는데 생각보다 신경 써야 하는 부분이 많았다. 이런 로직을 자주 접하면서 더 최적화할 수 있도록 해야겠다.

 

😸예제코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

728x90
728x90

 

이번 포스팅은 Service를 구현하고 그에 대한 테스트 코드를 작성할 것이다.

 

우선 Service를 구현하기 전에 계층 간 데이터 전송을 하기 위해 먼저 DTO를 구현한다.

 

🧷DTO

@Getter
@NoArgsConstructor
public static class SaveRequest{
   private String categoryName;
   private String parentCategory;
   private Map<String,CategoryInfo> subCategory;

   @Builder
   public SaveRequest(String categoryName, String parentCategory, Map<String, CategoryInfo> subCategory) {
       this.categoryName = categoryName;
       this.parentCategory = parentCategory;
       this.subCategory = subCategory;
     }
   public Category toEntity(){
          Category category = Category.builder()
                    .categoryName(categoryName)
                    .build();
            return category;
        }
 }
  • childCategory를 get 메서드를 통해 json형식으로 데이터를 넘길 때 , Map으로 넘겨준다.

🗞Service

createCategory()

@Transactional
public void createCategory(SaveRequest request){
   Boolean existByName = categoryRepository.existsByCategoryName(request.getCategoryName());
   if (existByName){
        throw new IllegalArgumentException("카테고리 중복");
   }

   if (request.getParentCategory() == null){
       SaveRequest rootCategory = SaveRequest.builder()
                .categoryName(request.getCategoryName())
                .parentCategory(null)
                .build();

         categoryRepository.save(rootCategory.toEntity());
   }else{
         String parentCategory1 = request.getParentCategory();
         Category parentCategory = categoryRepository.findByCategoryName(parentCategory1)
                 .orElseThrow();
         Category category = Category.builder()
                 .categoryName(request.getCategoryName())
                 .parentCategory(parentCategory)
                 .build();
        parentCategory.getChildCategory().add(request.toEntity());
        categoryRepository.save(category);
      }
 }
  • 우선 첫번째로 카테고리 이름이 중복되는지 검증을 해준다. 만약 같은 카테고리 이름이 존재한다면 예외를 던져준다.
  • 만약 부모 카테고리가 없는 경우는 null값을 넣어준다.(추후에 다른 Default값이 들어가게 구현 예정)
  • 부모 카테고리가 있다면 검색을 한 후 자식 카테고리에 현재 카테고리를 넣어준다.

 

다음은 조회 로직이다.

 

🗓GET

@Getter
@NoArgsConstructor
@Builder
public static class CategoryResponse{
    private Long id;
    private String categoryName;
    private Map<String,CategoryInfo> subCategory=new HashMap<>();

    public CategoryResponse(Long id, String categoryName, Map<String, CategoryInfo> subCategory) {
         this.id = id;
         this.categoryName = categoryName;
         this.subCategory = subCategory;
     }
 }

조회할 데이터를 받아올 DTO를 생성한다.

public CategoryDto.CategoryResponse toCategoryResponse(){
        return CategoryDto.CategoryResponse.builder()
                .id(this.id)
                .categoryName(this.categoryName)
                .subCategory(
                    this.childCategory.stream().collect(Collectors.toMap(
                            Category::getCategoryName, CategoryDto.CategoryInfo::new
                    ))
             )
            .build();
}

다음 Entity클래스에 조회시 받아올 데이터에 대한 값을 스펙을 설정해준다.

@Transactional(readOnly = true)
    public CategoryResponse getCategoryById(Long id){
        CategoryResponse response = categoryRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("찾는 카테고리가 존재하지 않음"))
                .toCategoryResponse();
        return response;
}

서비스 계층에 메서드를 생성하고 아이디로 저장소에서 카테고리를 찾고 없으면 예외를 던져준다.

(추후 이름으로 조회하는 로직도 구현 예정)

 

다음으로 삭제와 수정 로직인데 이 로직들은 딱히 복잡한 것이 없다.

 

🗓Delete

@Transactional
public void deleteCategory(Long id){
     Category category = categoryRepository.findById(id)
            .orElseThrow(IllegalArgumentException::new);
    categoryRepository.deleteById(id);
}
  • Id로 카테고리를 검색하고 만약 카테고리가 존재하지 않으면 예외를 던진다.
  • 그리고 자식 카테고리가 존재하는 경우 삭제할 수 없도록 예외를 던져준다.

 

🗓Update

 public void update(SaveRequest request){
        this.categoryName = request.getCategoryName();
        this.parentCategory = request.toEntity().getParentCategory();
    }

entity 클래스에 update 메서드를 생성한다.

 

@Transactional
public void updateCategory(Long id,SaveRequest request){
    Category category = categoryRepository.findById(id).orElseThrow(IllegalArgumentException::new);

     category.update(request);
}

우선 수정 로직은 저장과 비슷하지만 categoryRepository에서 id로 카테고리를 찾은 후 업데이트해준다.

 

 

🗓TestCode

  • 진짜 테스트 코드는 서비스가 커지고 로직이 복잡할수록 중요하다. 
  • 내가 개발했지만 서비스가 커지면 뭐가 뭔지 헷갈릴 때가 있다. 그러므로 핵심 로직을 통과하기 위한 데이터를 생성하여 비즈니스 로직을 디버깅하고, 문제를 발견하는 작업이 중요하다.

 

이제 Service에 대한 테스트 코드를 작성해보자

 

우선 기본 설정과 들어갈 데이터 값을 설정해준다.

@ExtendWith(MockitoExtension.class)
class CategoryServiceTest {

    @InjectMocks
    CategoryService categoryService;
    @Mock
    CategoryRepository categoryRepository;



    public CategoryDto.SaveRequest request1(){
        return CategoryDto.SaveRequest.builder()
                .categoryName("category1")
                .parentCategory(null)

                .build();
    }
}
  • @RunWith(MockitoExtension.class) : JUnit5부터 @ExtendWith(MockitoExtension.class)를 사용한다.
  • @InjectMocks : @Mock이 붙은 목객체를 @InjectMocks이 붙은 객체에 주입시킬 수 있다.
    @Mock : mock 객체를 생성해준다.
  • assertThat으로 값을 비교해준다.

 

⛏save_category()

@Test
@DisplayName("카테고리 생성")
void save_category()throws Exception{
    //given
    CategoryDto.SaveRequest request1 = request1();

    //when
    when(categoryRepository.existsByCategoryName(request1.getCategoryName()))
            .thenReturn(false);
    categoryService.createCategory(request1);

    //then
    Assertions.assertThat(request1.getCategoryName()).isEqualTo(request1().getCategoryName());
    Assertions.assertThat(request1.getParentCategory()).isEqualTo(null);

}

 

update_category()

@Test
@DisplayName("카테고리 수정")
void update_category()throws Exception{
    //given    
    Category category = request1().toEntity();
    Long id = category.getId();

    //when
    when(categoryRepository.findById(id)).thenReturn(Optional.of(category));
    categoryService.updateCategory(id,request2());

    //then
    Assertions.assertThat(category.getCategoryName()).isEqualTo(request2().getCategoryName());
}

 

delete_category()

@Test
@DisplayName("카테고리 삭제")
void delete_category()throws Exception{
    //given
    Category category = request1().toEntity();
    Long id = category.getId();

    //when
    when(categoryRepository.findById(id)).thenReturn(Optional.of(category));
    categoryService.deleteCategory(id);

    //then
    verify(categoryRepository,atLeastOnce()).deleteById(id);

}

 

 

테스트 코드를 실행하면 정상적으로 통과된다.

 

 


 

😸예제코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

 

 

728x90
728x90

 

토이 프로젝트를 진행하면서 카테고리를 만드는데 무한 뎁스로 만드는 과정을 기록하려 한다.

Stack : Java,Gradle, Spring data jpa ,Mysql

위 사진 처럼 유연하게 설계하여 무한하게 카테고리가 생성이 가능한 로직을 구현할것이다.

 

🔓요구조건 

  1. Entity 구현
    • 하나의 테이블에서 구현해야하기 떄문에 Self Join 사용하여 구현 
  2. Repository 인터페이스 구현
    • Spring data JPA를 사용을 위해 JpaRepository를 상속 받는다.
  3. DTO 구현
    • Entity는 DB를 생성하고 DB를 가지고 계층간의 소통을 하기 위해 DTO를 구현할 것이다.
    • Entity에서 list로 담은 childCategory는 DTO에서는 Map으로 구현한다.
  4. Service 구현 
    • Save : 카테고리를 생성하는 로직이다.( 카테고리 이름이 중복되는지 검증 후 생성,부모 자식 카테고리를 가지고 있는지 검증 )
    • delete :  카테고리를 삭제하는 로직이다. ( 자식 카테고리가 존재하면 삭제할 수 없다.)
    • update : 카테고리를 업데이트하는 로직이다.
    • get : 카테고리를 Id와 name으로 조회하는 로직이다. (자식 카테고리 존재시 같이 조회된다.)
  5. Service Test Code 구현 
    • 테스트 코드는 항상 중요하다.
    • 서비스 단위 테스트 코드이다.
  6. Controller 구현
    • Api 통신을 위해 반환 로직을 구현

 

 

1. Entity 구현 

  • 하나의 테이블에서 무한 뎁스를 구현해야하기 때문에 Self Join을 사용할 것이다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Category {

    @Id@GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "category_id")
    private Long id;

    private String categoryName;

    @ManyToOne(fetch = FetchType.LAZY)
    private Category parentCategory;

    @OneToMany(mappedBy = "parentCategory")
    private List<Category>childCategory =new ArrayList<>();

 

    @Builder
    public Category(String categoryName, Category parentCategory, List<Category> childCategory) {
        this.categoryName = categoryName;
        this.parentCategory = parentCategory;
        this.childCategory = childCategory;

    }
}
  • 여기서 주의깊게 볼것은 parentCategory childCategory이다. 
  • 서로 물고 무는 무한뎁스 구조를 위해 두 칼럼끼리 연관관계를 맺고 mappedBy속성을 통해서 parentCategory를 연관 관계 주인으로 설정해준다.(주인을 설정해주지 않으면 조회시 계속 서로를 참조하는 무한루프가 발생한다!)
  • JPA에서 연관관계를 설정할때 ManyToOne,OneToOne은 fetchType.EAGER이 default이다. 그래서 지연 참조(detchType.Lazy)를 사용해줘야 N+1 이슈 등 문제가 생기지 않는다.

2.Repository

public interface CategoryRepository extends JpaRepository<Category,Long> {
    Optional<Category>findByCategoryName(String name);
    Boolean existsByCategoryName(String name);
}
  • extends JpaRepository는 Spring data jpa를 사용하기 위해 무조건 상속 받아야한다.
  • Spring data jpa는 메서드 이름명으로 검색 조건을 만들수 있다.(find + By + Entity 필드값)
  • existsByCateoryName은 카테고리 이름이 중복되는지 검증하기 위해 Boolean 타입으로 리턴 받는다.

 

다음 포스팅은 서비스와 서비스에 대한 테스트 코드를 포스팅할 예정이다.

 

😸예제 코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

728x90
728x90

💻AssertJ란

AssertJ는 많은 assertion을 제공하는 자바 라이브러리이다. 오류 메세지와 테스트 코드의 가독성을 높이고 자신이 사용하는 IDE에서 사용하기에 용이하다.

AssertJ는 사이드 프로젝트로 만들어졌다. 만약 무엇인가 잘못된것이 있다면 언제든지 피드백을 할 수 있다고 원작자가 말하였다.


가독성 이야기를 해보자면 

//JUnit
assertEquals(expected, actual); 

//AssertJ
assertThat(actual).isEqualTo(expected);

두 예제가 있는데 첫번째가 JUnit에서 제공하는 assertEquals이다. 아래는 AssertJ의 AssertThat이다.

AssertThat이 확실히 직관적으로 보이고 가독성이 올라간다.

 

가장 기본적인 예제

@Test 
void simple_case() { 
	assertThat("ABCD").isNotNull() 
    	    .startsWith("A")
            .contains("BC") 
            .endsWith("D"); 
         
}

봐도 어느정도는 어떤말인지 감은 온다. "ABCD"라는 문자열이 NULL이 아니고 "A"로 시작하고 "BC"가 포함되어 있고 "D"로 끝난다는 테스트 예제이다.

 

이제 다양한 예제를 보자


💻Filtering assertions

@Test
void filter_test1(){

    List<Human> list = new ArrayList<>();
    Human k = new Human("Kim",22);
    Human a = new Human("Aim",23);
    Human p = new Human("Park",42);
    Human j = new Human("Jin",12);
    Human y = new Human("yun",32);

    list.add(k);
    list.add(a);
    list.add(p);
    list.add(j);
    list.add(y);

    Assertions.assertThat(list).filteredOn(human ->
                human.getName().contains("i"))
                .containsOnly(a,j,k);

}

예를 보면 Human클래스에는  이름과 나이가 있다. filteredOn은 직관적으로 걸러낸다?이렇게 생각하면 쉬울것이다.

이름을 모두 가져와서 이름중에 i가 포함되는 이름을 걸러낸다. 그 다음 containOnly로 a,j,k 가 맞는지 검증한다.

 

💻객체의 property를 검증 예제

@Test
void filter_test2() {
   
     List<Human> list = new ArrayList<>();
     Human k = new Human("Kim",22);
     Human a = new Human("Aim",23);
     Human p = new Human("Park",42);
     Human j = new Human("Jin",12);
     Human y = new Human("yun",32);

    
     list.add(k);
     list.add(a);
     list.add(p);
     list.add(j);
     list.add(y);

     Assertions.assertThat(list).
                filteredOn("age", 22)
                .containsOnly(k);
}

위와 세팅은 같다. assertThat 구문에서 프로퍼티에 값에 접근하여 값을 검증한다.

 

값이 포함되지 않은 경우도 검증을 할 수 있다.

@Test
void filter_test3() {

        List<Human> list = new ArrayList<>();
        Human k = new Human("Kim",22);
        Human a = new Human("Aim",23);
        Human p = new Human("Park",42);
        Human j = new Human("Jin",12);
        Human y = new Human("yun",32);

        list.add(k);
        list.add(a);
        list.add(p);
        list.add(j);
        list.add(y);

        Assertions.assertThat(list)
                .filteredOn("age", notIn(22))
                .containsOnly(k);
}

notIn안에 들어간 값이 없는 객체만 검증하는것이다. 

나머지로는 In,not등이 있다. not과 notIn은 거의 동일하다.

 

💻property 추출하기 

extracting을 사용하면 테스트를 할때  객체 이름을 검증하기 위해 반복문에서 이름을 꺼내고 또 다른 리스트에 담고 비교하는 불편한 과정을 간편하게 해결할 수 있다.

 

@Test
void filter_test4() {

      List<Human> list = new ArrayList<>();
      Human k = new Human("Kim",22);
      Human a = new Human("Aim",23);
      Human p = new Human("Park",42);
      Human j = new Human("Jin",12);
      Human y = new Human("yun",32);

      list.add(k);
      list.add(a);
      list.add(p);
      list.add(j);
      list.add(y);

      Assertions.assertThat(list).extracting("name")
                .contains("Kim","Aim","Park","Jin","yun");
}

list를 넘겨도 list에서 어떤 함수를 부르고 걸러진 값들에 대한 필드를 추출(extracting)해서 검증할 수 있다.

 

또, 필드명뿐만 아니라 클래스 타입을 명시하여 검증을 강하게 할 수 있다.

@Test
void filter_test5() {

      List<Human> list = new ArrayList<>();
      Human k = new Human("Kim",22);
      Human a = new Human("Aim",23);
      Human p = new Human("Park",42);
      Human j = new Human("Jin",12);
      Human y = new Human("yun",32);

      list.add(k);
      list.add(a);
      list.add(p);
      list.add(j);
      list.add(y);

      Assertions.assertThat(list).extracting("name",String.class)
                .contains("Kim","Aim","Park","Jin","yun");
    }

 

그리고 튜플로도 추출이 가능하다. 여러개의 필드를 검증할 때 유용하다.

@Test
void filter_test6() {
     
      List<Human> list = new ArrayList<>();
      Human k = new Human("Kim",22);
      Human a = new Human("Aim",23);
      Human p = new Human("Park",42);
      Human j = new Human("Jin",12);
      Human y = new Human("yun",32);

      list.add(k);
      list.add(a);
      list.add(p);
      list.add(j);
      list.add(y);

     Assertions.assertThat(list).extracting("name","age")
                .contains(tuple("Kim",22)
                        ,tuple("Aim",23)
                        ,tuple("Park",42)
                        ,tuple("Jin",12)
                        ,tuple("yun",32));
    }

 

💻String assertions

문자열 검증 예시이다.

@Test
void stringAssertions(){
      String s = "ABCDE";

      Assertions.assertThat(s).startsWith("A").contains("BCD").endsWith("E");

}

 

💻DBB 스타일

@Test
void DBB_st()throws Exception{
        //given
        
        //when
        Throwable t = catchThrowable(()-> {
            throw new Exception("Exception");
        });
        
        //then
        Assertions.assertThat(t).isInstanceOf(Exception.class)
                .hasMessageContaining("Exception");
        
    
}

given.when.then 주석으로 가독성이 높고 순차적으로 어디서 어떤 작업을 하는지 알기 용이하다.

 

 

💻Exception test(테스트 예외 처리)

@Test 
public void exception_assertion() { 
	assertThatThrownBy(() -> { 
    		throw new Exception("exception"); 
            	}).isInstanceOf(Exception.class) 
                .hasMessageContaining("exception"); };

assertThatThrownBy()는 Trowable보다 가독성이 높게 작성할 수 있다.

 

예외 처리 문법 예제

@Test 
public void exception_assertion() { 
    assertThatIOException().isThrownBy(() -> {
    	throw new IOException("exception"); 
        }) 
        .withMessage("exception") 
        .withMessageContaining("exception") 
        .withNoCause(); 
 }
@Test 
public void testException() { 
    assertThatExceptionOfType(IOException.class).isThrownBy(() -> { 
    	throw new IOException("exception"); 
       }) 
        .withMessage("exception") 
        .withMessageContaining("exception") 
        .withNoCause(); 
}

 

🧑🏻‍💻예제 코드 : https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com


⌨️REFERENCE

https://pjh3749.tistory.com/241

728x90

'Dev > TestCode' 카테고리의 다른 글

[TestCode] JUnit5 기본 메뉴얼  (0) 2021.12.05