Spring/Spring boot

[25] Spring Data : Spring Data - JPA 연동

낙타선생 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로 진행하는 것이 안전하고 빠릅니다.