ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA로 CRUD 해보기 + 테스트코드
    프로젝트/게시판 프로젝트 2022. 4. 28. 22:15

    https://junuuu.tistory.com/270?category=997278 

     

    Spring Initializr로 프로젝트 생성하기 + MySQL연동까지

    https://start.spring.io/ Proejct : Gradle Project Language : Java Packaging : Jar(REST API server로 만들기 때문에 JSP필요 x) - Spring 문서에서도 JSP를 피하라고 명시 Java : 11(16으로 변경해서 사용 예..

    junuuu.tistory.com

     

    이전에 이어서 Spring Initailzr로 스프링 부트 프로젝트를 생성했으며 MySQL까지 연동된 상황입니다.

     

    지난번에 resources/application.properties 에서 JPA에 대한 설정을 할때 다음과 같은 설정을 했습니다.

    spring.jpa.hibernate.ddl-auto=update

     

    • create  :  기존 테이블을 삭제하고 새로 생성 [ DROP + CREATE ]
    • create-drop  :  CREATE 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거  [ DROP + CREATE + DROP ]
    • update  :  DB 테이블과 엔티티 매핑 정보를 비교해서 변경 사항만 수정 [ 테이블이 없을 경우 CREATE ] 
    • validate  :  DB 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않음
    • none  :  자동 생성 기능을 사용하지 않음

    여기서 update를 선택했기 때문에 DB 테이블과 엔티티 매핑 정보를 비교해서 변경 사항만 수정하고 테이블이 없을 경우에는 CREATE 해준다는 것을 알 수 있습니다.

     

    이제 Entity를 생성해 보겠습니다.

    원래 계획된 회원정보에는 여러가지가 있지만 현재는 테스트 목적으로 PK인 member_Id, user_Id만 생성해 보겠습니다.

    package anthill.Anthill.domain;
    import lombok.*;
    import javax.persistence.*;
    import static lombok.AccessLevel.PROTECTED;
    
    @NoArgsConstructor(access = PROTECTED)
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder
    @Getter
    @Table(name = "member")
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY) //MySQL의 AUTO_INCREMENT를 사용
        @Column(name="member_Id")
        private Long id;
    
        @Column(name ="user_Id", nullable = false, unique = true, length = 20)
        String userId;
    
    }

     

     

    @Entity

    DB 테이블을 뜻합니다.

    JPA Repository를 사용하기 위해서는 반드시 @Entity 어노테이션이 명시되어 있어야 합니다.

     

    @Table

    DB 테이블명을 명시했습니다.

    테이블 명과 클래스 명이 동일한 경우 생략 가능하지만 명시적으로 보일 수 있도록 기입해 주었습니다.

     

    @Getter

    Lombok의 어노테이션인 @Getter입니다.

    해당 어노테이션은 Getter 메서드인 getXXX 메서드들을 생성해줍니다.

    ex) member.getUserId(), member.getId()

     

    @Builder

    Lombok의 어노테이션인 @Builder입니다.

    Builder 어노테이션을 사용하기 위해서는 @AllArgsConstructor와 @NoArgsConstructor가 필요합니다.

    생성 패턴인 빌더 패턴을 편리하게 사용하기 위한 어노테이션이며 해당 어노테이션에 대해서는 좀 더 뒤에 자세하게 다루어 보겠습니다.

     

     

    @Id

    DB 테이블의 Primary Key를 뜻합니다.

     

    @GeneratedValue

    Primary Key의 키 생성 전략(Strategy)을 설정하고자 할 때 사용

    • GenerationType.IDENTITY : MySQL의 AUTO_INCREMENT 방식을 이용
    • GenerationType.AUTO(default) : JPA 구현체(Hibernate)가 생성 방식을 결정
    • GenerationType.SEQUENCE : DB의 SEQUENCE를 이용해서 키를 생성. @SequenceGenerator와 같이 사용
    • GenerationType.TABLE : 키 생성 전용 테이블을 생성해서 키 생성. @TableGenerator와 함께 사용

    여기에서 memberID는 AUTO_INCREMENT 방식을 사용하기 때문에 GenerationType.IDENTITY 방식을 사용했습니다.

     

    @Column

    DB Column을 이름을 명시합니다.

    여기서는 member_Id로 명시했습니다.

    @Column과 반대로 테이블에 칼럼으로 생성되지 않는 필드의 경우엔 @Transient 어노테이션을 적용한다.

     

    또한 userId의 경우에는 null값이면 안되기 때문에 nullable = false, 항상 유일한 값을 가져야 하기 때문에 unique = true, varchar의 길이는 20으로 설정하였습니다.

     

    이렇게 Member 클래스를 작성하게 되고 스프링을 실행한 후 DB를 확인해보면 Member 테이블이 생성되어 있습니다!


    이제 JPA Repository를 만들어 보겠습니다.

    package anthill.Anthill.repository;
    
    import anthill.Anthill.domain.Member;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    
    
    public interface MemberRepository extends JpaRepository<Member, Long> {
    }

    끝입니다.

    MemberRepository에 JPARepository <Entity, PK의 타입>을 상속해주면 끝입니다.

    MemberRepository를 이용해서 작성된 테이블에 SQL문 없이 CRUD 작업을 할 수 있게 됩니다.

    Spring Data JPA는 자동으로 스프링의 빈(bean)으로 등록됩니다.

     

    만약 나는 userId로도 조회하고 싶은데? 라면 아래와 같이 추가만 해주면 끝입니다.

    public interface MemberRepository extends JpaRepository<Member, Long> {
        Member findByUserId(String userId);
    }

    <반환타입> findBy{변수명}(<타입> 인자명)을 선언만 하면 알아서 JPA가 구현해 줍니다.

     

    Entity와 JPA Repository를 만들었습니다. 이제 테스트를 해보겠습니다.

    package anthill.Anthill.repository;
    
    import anthill.Anthill.domain.Member;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.dao.DataIntegrityViolationException;
    
    
    import javax.transaction.Transactional;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.IntStream;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    @Transactional
    @SpringBootTest
    class MemberRepositoryTest {
    
        @Autowired
        MemberRepository memberRepository;
    
        @DisplayName("CREATE 테스트")
        @Test
        public void insertSuccessTest() {
            IntStream.rangeClosed(1, 10).forEach(i -> {
                Member member = Member.builder().userId("Test" + i).build();
                memberRepository.save(member);
            });
    
            List<Member> members = memberRepository.findAll();
            Assertions.assertThat(members.size()).isEqualTo(10);
    
        }
    
        @DisplayName("DELETE 테스트")
        @Test
        public void deleteSuccessTest() {
            //given
            Member member = Member.builder().userId("Test").build();
            memberRepository.save(member);
    
            //when
            memberRepository.deleteById(member.getId());
    
            //then
            List<Member> members = memberRepository.findAll();
            Assertions.assertThat(members.size()).isEqualTo(0);
        }
    
        @DisplayName("READ 테스트")
        @Test
        public void selectSuccessTest() {
            //given
            Member member = Member.builder().userId("test").build();
            Member fail = Member.builder().userId("fail").build();
            memberRepository.save(member);
            //when
            Optional<Member> result = memberRepository.findById(member.getId());
    
            //then
            Assertions.assertThat(member.getUserId()).isEqualTo(result.orElse(fail).getUserId());
        }
    
        @DisplayName("UPDATE 테스트")
        @Test
        public void updateSuccessTest() {
            //given
            Member member = Member.builder().userId("test").build();
            Member fail = Member.builder().userId("fail").build();
            memberRepository.save(member);
    
            //when
            Member updateMember = Member.builder().id(member.getId()).userId("updated").build();
            memberRepository.save(updateMember);
    
            //then
            Optional<Member> result = memberRepository.findById(member.getId());
            Assertions.assertThat(result.orElse(fail).getUserId()).isEqualTo("updated");
        }
    
        @DisplayName("데이터 무결성 테스트")
        @Test
        public void insertFailTest() {
            //given
            Member member = Member.builder().build();
            
            //then
            assertThrows(DataIntegrityViolationException.class, () -> {
                //when
                memberRepository.save(member);
            });
        }
    
    }

    @SpringBootTest

    해당 어노테이션은 애플리케이션 테스트에 필요한 거의 모든 의존성들을 제공합니다.

    Spring Main Application인 @SpringBootApplication을 찾아가 하위의 모든 Bean을 Scan 합니다.

     

    @Transcational

    우리는 테스트할 때 재사용할 수 있는 테스트를 작성해야 합니다.

    하지만 DB에 Insert 하게 되면 다시 테스트하기 위해서는 일일이 DB값을 지워줘야 합니다.

    https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-rollback-and-commit-behavior

     

    하지만 테스트에 @Transcational 어노테이션을 사용한다면 테스트가 완료되고 나면 자동으로 rolled back 되어 테스트 코드에 사용한 데이터를 관리하기 쉽습니다.

     

    만약에 나는 DB에 실제로 데이터가 들어가는 걸 보고 싶다면?

    @Transcational 어노테이션을 사용하지 않으면 됩니다.

     

     

    @Autowired

    MemberRepository를 사용하기 위해 객체를 주입받습니다.

     

     

    테스트 메서드를 간단하게 설명해보겠습니다.

    insertTest

    10개의 Member 데이터를 저장하고, findAll()으로 불러와 해당 데이터가 10개인지를 검증

     

    deleteTest

    1개의 데이터를 저장하고, 그 데이터를 삭제합니다. 이후에 findAll()으로 불러와 데이터가 0개인지를 검증

     

    selectTest

    test라는 userId를 가지는 Member 데이터를 저장합니다.

    이후에 해당 member의 ID를 통해 Member를 조회하고 그 결과가 test라는 userID를 가지는 Member인지 검증합니다.

     

    updateTest

    test라는 userID를 가지는 Member 데이터를 저장합니다.

    이후에 같은 memberID를 가지며 updated라는 userID를 가지는 Member 데이터를 저장합니다.

    JPA는 내부적으로 해당 엔티티의 @Id값이 일치하는지를 확인해서 insert 혹은 update 작업을 처리합니다.
    여기에서는 update작업이 발생하게 됩니다.

    이후에 처음에 test라는 userID를 가지는 유저를 조회했을 때 userID값이 updated인지 검증합니다.

     

    dataIntegrityTest

    userID는 Not Null입니다.

    즉, DB에서도 꼭 넣어줘야 하는 값인데 이 값을 가지지 않고 들어가면 어떻게 될까 궁금하여 빈값으로 save()를 호출해보니 DataIntegrityViolationException이 발생했습니다.

    Exception 발생

    따라서 assertThrows를 사용하여 해당 Exception이 터지는지 검증합니다.

     

    결과

    테스트 코드 통과

     

    출처

    https://dev-coco.tistory.com/85

     

    [Spring Boot] MySQL & JPA 연동 및 테스트 (Gradle 프로젝트)

    SpringBoot에서 MySQL 그리고 Spring Data JPA를 연동하는 방법에 대해 알아보도록 하겠습니다. 1. 프로젝트에 의존성 추가하기 build.gradle에 의존성을 아래와 같이 추가해줍니다. dependencies { implementatio..

    dev-coco.tistory.com

    https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-rollback-and-commit-behavior

     

    Testing

    The classes in this example show the use of named hierarchy levels in order to merge the configuration for specific levels in a context hierarchy. BaseTests defines two levels in the hierarchy, parent and child. ExtendedTests extends BaseTests and instruct

    docs.spring.io

     

    댓글

Designed by Tistory.