ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [25] Spring Data : Spring Data - JPA 연동
    Spring/Spring boot 2020. 7. 15. 08:53
    반응형

    [Spring Data JPA의 사용의 기본]

    1. pom.xml file에 org.springframework.boot:spring-boot-starter-data-jpa 의존성을 추가해줍니다.

    ...
    
        <dependencies>
            
            ...
            
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            
            ...
            
        </dependencies>
    
    ...

     

     

     

    2. 본 Post에서 db는 docker를 통한 postgresql을 사용합니다. (postgresql 관련 포스트)  docker를 통해 postgresql을 먼저 실행해주세요. postgresql을 사용하기 위한 의존성을 pom.xml에 추가해줍니다.

    ...
    
        <dependencies>
            
            ...
            
            <dependency>
                <groupId>org.postgresql</groupId>
                <artifactId>postgresql</artifactId>
            </dependency>
            
            ...
            
        </dependencies>
    
    ...

    application.properties file에 spring.datasource.* property들을 지정하지 않으면 기본적으로 내장 database를 사용하게 됩니다. 여기서는 docker에 실행 중인 postgresql을 사용할 것임으로 접속에 필요한 property를 다음과 같이 지정해줍니다.

    spring.datasource.hikari.maximum-pool-size=2
    
    spring.datasource.url=jdbc:postgresql://localhost:5432/springboot
    spring.datasource.username=dave
    spring.datasource.password=pass 
    
    #Connection driver가 create clob을 지원하지 않아서 발생하는 경고를 무시하도록 합니다.
    spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

     

     

     

    3. 이제 DB table과 mapping할 class를 하나 만듭니다. 이 Entity class는 bean으로 등록되기 때문에 getter/setter를 추가해줍니다. 또한 eqauls, hashCode method를 추가해주면 좋습니다.

    @Entity	//	①
    public class Users {
    
        @Id	//	②
        @GeneratedValue	//	③
        private Long id;
    	
        @Column(length = 100, nullable = false)	//	④
        private String username;
    
        private String password;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Users users = (Users) o;
            return Objects.equals(id, users.id) &&
                    Objects.equals(username, users.username) &&
                    Objects.equals(password, users.password);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(id, username, password);
        }
    }
    

    ① DB table과 연결될 class임을 나타냅니다. camel case로 명명된 class명 중 대문자 앞에 언더스코어(_)를 삽입한 table로 매칭 되는 것이 기본 설정입니다. (예) PackageGoods.java  -> package_goods table

     

    ② table의 primary key가 될 member변수 입니다.

     

    ③ primary key의 생성 규칙을 정의합니다. spring boot 2.0에서는 GenerationType.IDENTITY option을 추가해줘야 자동 증가가 됩니다.

     

    ④ table의 column property를 변경하기 원하는 경우 사용합니다. 선언하지 않아도 Entity class의 모든 member변수는 column이 됩니다.

     

     

     

     

    4. Repository interface 없이 Entity class만으로는 아무것도 할 수 없습니다. table에 대해 query 하기 위한 method를 정의하기 위해 Repository interface를 만듭니다. 

    public interface UsersRepository extends JpaRepository<Users, Long> {	//	①
    
    }

    ① JpaRepository<Entity_class_type, Primary_key_type> generic type에는 Entity class로 연결된 table에 대한 CRUD method가 정의되어 있습니다. 이 generic type을 상속받아 interface를 작성하면 해당 CRUD method를 모두 사용할 수 있습니다.

     

     

     

     

    5. Repository interface의 method를 검증하기 위한 비어있는 test code를 작성합니다.

    @RunWith(SpringRunner.class)
    @DataJpaTest    //	①
    public class UsersRepositoryTest {
    
        @Autowired
        DataSource dataSource;	//	②
    
        @Autowired
        JdbcTemplate jdbcTemplate;	// ②
    
        @Autowired
        UsersRepository usersRepository;	// ③
    
        @Test
        public void usersRepository테스트() {
    
    
        }
    }

    ① DataJpa에 관한 slicing test로 repository 및 이와 관련된 bean만 주입받아 test를 진행하도록 하는 annotation입니다.

    ② @DataJpaTest에 의해 DataSoruce와 JdbcTemplate bean을 주입받을 수 있습니다.

    ③ test를 진행하기 위한 Repository interface를 bean으로 주입받습니다.

     

    test 내용이 비워진 상태로 test를 진행해봅니다. 이를 통해 bean이 잘 주입되는지 test method에는 문제가 없는지 등을 확인해 볼 수 있기 때문입니다.

     

    test를 진행하면 다음과 같은 error message와 함께 통과하지 못하는 것을 볼 수 있습니다. 

    test를 진행하기 위해 내장 database를 사용해야 하는데 의존성에 내장 database 관련 의존성을 추가하지 않았기 때문에 발생한 문제입니다. 이를 해결하기 위해 다음과 같이 내장 database h2에 대한 의존성을 pom.xml에 추가합니다.

    ...
    
    <dependencies>
    
    	...
        
    	<dependency>
    		<groupId>com.h2database</groupId>
    		<artifactId>h2</artifactId>
    		<scope>test</scope>
    	</dependency>
    
    	...
        
    </dependencies>
    
    ...

     

    다시 test를 돌려보면 문제없이 통과됩니다.

     

     

     

    6. 임의의 data를 insert 하고 정상적으로 select 되는지 확인해보는 test code를 작성합니다.

    import static org.assertj.core.api.Assertions.assertThat;
    
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class UsersRepositoryTest {
    
        @Autowired
        DataSource dataSource;
    
        @Autowired
        JdbcTemplate jdbcTemplate;
    
        @Autowired
        UsersRepository usersRepository;
    
        @Test
        public void usersRepository테스트() throws SQLException {
    
            try(Connection connection = dataSource.getConnection()) {	//	①
                DatabaseMetaData metaData = connection.getMetaData();	//	②
                System.out.println(metaData.getURL());	//	②
                System.out.println(metaData.getDriverName());	//	②
                System.out.println(metaData.getUserName());	//	②
    
                Users users = new Users();	//	③
                users.setUsername("dave");	//	③
                users.setPassword("pass");	//	③
    
                Users insertedUsers = usersRepository.save(users);	//	④
    
                assertThat(insertedUsers).isNotNull();	//	⑤
    
                Users selectedUsers = usersRepository.findByUsername(insertedUsers.getUsername());	//	⑥
                assertThat(selectedUsers).isNotNull();	//	⑦
    
                Users notInsertedUsers = usersRepository.findByUsername("Mike");	//	⑧
                assertThat(notInsertedUsers).isNull();	//	⑨
    
            }
        }
    }
    

    ① database connection을 얻습니다. try 구문을 사용하여 resource를 사용 후 자동으로 반환되도록 하였습니다.

     

    ② database connection의 metadata를 받아서 URL, DriverName, UserName을 출력합니다.

     

    ③ database에 insert 하고자 하는 data를 Entity class에 담습니다.

     

    ④ save() method는 Spring Data JPA가 구현해주는 CRUD method중 하나입니다. 이를 사용하여 Entity class에 담아놓은 data를 database에 insert 하도록 요청합니다.

     

    ⑤ save() method가 정상 동작하면 Entity class type의 객체를 반환하게 됩니다. null이 아니면 정상적으로 실행된 것입니다.

     

    findByUsername()는 usersRepository interface에 새롭게 정의하게 될  method입니다.  Spring Data Jpa는 이렇게 Repository interface에 정의한 method의 구현체를 만들어줍니다. 추가된 findByUsername()은 method 명에서 알 수 있듯이 이름으로 user를 찾아 Users 객체로 반환해주는 method입니다. 여기서는 insert한 username에 해당하는 user를 찾아 Users 객체로 반환해줍니다.

     

    ⑦ insert했던 username과 동일한 user를 select한 결과가 있는지 확인합니다. (여기서는 반드시 존재해야 합니다.)

     

    ⑧ 한 번도 insert한 적이 없는 데이터를 select 하도록 합니다.

     

    ⑨ 해당되는 data가 없어야 하므로 그 결과가 null 될 것을 기대합니다.

     

     

     

     

     

    7. Repository interface에 findByUsername() method를 추가합니다.

    public interface UsersRepository extends JpaRepository<Users, Long> {
    
        Users findByUsername(String username);
    }

     

     

    이제 text code를 실행해보면 정상 실행되고 이를 통해 원하는 data의 insert와 select가 문제없이 진행되는 것을 확인할 수 있습니다.

     

     

     

     

     

     


    [JPA Repository interface에서 Query 사용하기]

    JPA에서 제공하는 method만으로는 섬세한 qeury 작성에 어려움이 있기 때문에 실행할 qeury를 직접 작성하고 method에 binding하여 ㅡmethod 실행 시 해당 qeury가 실행되도록 할 수 있습니다. method에 @Query annotation을 붙이고 매개 값으로 실행될 query를 전달하면 JPA를 통해 해당 query 실행할 수 있습니다. query는 두 가지 문법으로 작성 가능합니다.

    1. Java Persistence Query Language (JPQL) : SQL과 비슷한 문법의 객체지향 Query
    2. Native Query : 사용하는 DB의 Query

    Native Query를 사용하는 예

    public interface UsersRepository extends JpaRepository<Users, Long> {
    
        @Query(nativeQuery = true, value = "select * from users where username = :username")
        Users findByUsername(String username);
    }

     

     

     

     

     

     


    [Optional<T> Repository interface의 method 반환 type으로 사용하기]

    Repository interface methode의 retrun value type을 null 처리를 편하게 할 수 있는 generic type인 Optional<T>로 줄 수 있습니다.

    Optional<T> type에 대한 내용은 여기를 참고해주세요.

     

    Repository interface를 다음과 같이 변경합니다.

    import java.util.Optional;
    
    public interface UsersRepository extends JpaRepository<Users, Long> {
    
        Optional<Users> findByUsername(String username);	//	①
    }

    ① query 결과를 Optional<T> type으로 반환합니다. Optional<T> type 특성 상 만약 값이 없으면 null 대신 empty가 됩니다.

     

     

    test code를 다음과 같이 변경합니다.

    import static org.assertj.core.api.Assertions.assertThat;
    
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class UsersRepositoryTest {
    
        @Autowired
        DataSource dataSource;
    
        @Autowired
        JdbcTemplate jdbcTemplate;
    
        @Autowired
        UsersRepository usersRepository;
    
        @Test
        public void usersRepository테스트() throws SQLException {
    
            try(Connection connection = dataSource.getConnection()) {
                DatabaseMetaData metaData = connection.getMetaData();
                System.out.println(metaData.getURL());
                System.out.println(metaData.getDriverName());
                System.out.println(metaData.getUserName());
    
                Users users = new Users();
                users.setUsername("dave");
                users.setPassword("pass");
    
                Users insertedUsers = usersRepository.save(users);
    
                assertThat(insertedUsers).isNotNull();
    
                Optional<Users> selectedUsers = usersRepository.findByUsername(insertedUsers.getUsername());
                assertThat(selectedUsers).isNotEmpty();	//	①
    
                Optional<Users> notInsertedUsers = usersRepository.findByUsername("Mike");
                assertThat(notInsertedUsers).isEmpty();	//	②
    
            }
        }
    }

    ① Optional<T> type으로 Users 객체를 받아왔으므로 null 대신 empty인지 확인합니다. insert한 값과 동일한 값으로 select하였으므로 Not Empty가 기대됩니다.

     

    Optional<T> type으로 Users 객체를 받아왔으므로 null 대신 empty인지 확인합니다. insert한 값과 다른 값으로 select하였으므로 Empty가 기대됩니다.

     

     

    test code를 실행하면 문제없이 pass합니다.

     

     

     

     

     


    [Integration test]

    위의 test는 integration test로 진행할 수도 있습니다. integration test로 진행하기 위해서는 @DataJpaTest 대신 @SpringBootTest annotation을 사용합니다. 하지만 integration test는 다음과 같은 몇 가지 단점이 존재합니다.

    1. 별도의 설정을 해주지 않으면 운영 중인 database를 가지고 test를 진행하게 됩니다.
    2. 설정을 한다고 해도 별도의 database를 필요로 합니다. 
    3. @SpringBootApplication 부터 모든 bean 설정을 진행하게 되어 test 시간이 길어집니다.

    위의 1번을 해결하기 위해 @SpringBootTest를 통해 integration test를 진행할 경우 test directory 안에 application.properties file을 추가하여 test용 DB로 설정해서 쓰거나 annotation 매개 값으로 properties를 지정할 필요가 있습니다.

    (예) @SpringBootTest(properties = "spring.datasource.url='{Test DB의 URL}'")

     

    test에 데이터를 누적해가는 것이 필요한 경우가 아니라면 slicing test로 진행하는 것이 안전하고 빠릅니다.

    댓글

Designed by Tistory.