-
[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는 두 가지 문법으로 작성 가능합니다.
- Java Persistence Query Language (JPQL) : SQL과 비슷한 문법의 객체지향 Query
- 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는 다음과 같은 몇 가지 단점이 존재합니다.
- 별도의 설정을 해주지 않으면 운영 중인 database를 가지고 test를 진행하게 됩니다.
- 설정을 한다고 해도 별도의 database를 필요로 합니다.
- @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로 진행하는 것이 안전하고 빠릅니다.
'Spring > Spring boot' 카테고리의 다른 글
[27] Spring Data : Database Migration Tool (FlyWay) (0) 2020.07.21 [26] Spring Data : Initialization of Database (0) 2020.07.17 [24] Spring Data : ORM, JPA, Spring-Data-JPA 개요 (0) 2020.07.13 [23] Spring Data : PostgreSQL 설정하기 (0) 2020.07.11 [22] Spring Data : MySQL 설정하기 (0) 2020.07.09