본문 바로가기
ORM/Spring Data JPA

기본 CRUD - READ

by 코딩하는 랄로 2024. 1. 16.
728x90

JPA를 사용할 때 주의할 점

JPA는 편리하게 Java 코드만으로 영속성 데이터를 다루게 해준다. 또한 Spring Data JPA는 JPA를 구현한 Hibernate 를 다시 한번 감싸서 사용자가 더 편리하게 여러 메소드를 통해 영속성 데이터를 다루게 해준다. 또한, 현재 사용하고 데이터베이스의 종류가 다르더라도 사용자가 사용하여야 할 메소드는 변하지 않는다!! ( 데이터베이스 종류에 독립적임!! )

 

이러한 점은 JPA의 큰 장점이지만 반대로 JPA을 사용할 때 주의하여야 하는 이유이다. 데이터베이스에 독립적으로 동작하여야 하기 때문에 JPA는 모든 데이터베이스에서 사용할 수 있는 쿼리문을 생성하기 때문에 몇몇 메소드들은 성능을 크게 저하시킬 수 있는 쿼리문을 생성한다.

 

그렇기 때문에 JPA를 사용할 때에는 사용하는 메소드가 어떠한 쿼리문을 생성하여 동작시키는 지를 알아야 한다. JPA는 사용자가 직접 Query문을 작성할 수 있도록 하는 방법도 제공하기 때문에 이러한 방법들을 적재적소에 사용하면, 프로그램의 성능을 올리면서 가독성 좋은 코드를 작성할 수 있을 것이다.

 

 

 

사전 작업

CRUD의 READ에 해당하는 메소드를 알아보기 전에, JpaRepository를 사용하기 위한 몇가지 작업이 필요하다. 먼저 Entity로 사용할 간단한 Student 클래스이다.

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private String address;

}

 

name과 address 필드를 파라미터로 받는 생성자를 만들기 위해 @NonNull annotation을 붙여주고 @RequiredArgsConstructor를 통해 생성자를 생성해주었다. 다음은 Student Entity를 다룰 Repository 클래스를 다음과 같이 생성하여 주자.

public interface StudentRepository extends JpaRepository<Student, Long> {

}

 

Student의 @Id 변수의 타입이 Long이기 때문에 JpaRespository의 ID type을 Long으로 지정해준다. JpaRepository를 생성하여 주었으면, 이제 여러 메소드를 테스트할 Test 클래스를 생성하자. 

@SpringBootTest
class StudentRepositoryTest {

    @Autowired
    private StudentRepository studentRepository;

    @BeforeEach
    void before() {
        List<Student> studentList = List.of(
            new Student("kim", "address1"),
            new Student("lee", "address2"),
            new Student("park", "address3")
        );

        studentRepository.saveAllAndFlush(studentList);
    }

}

 

JpaRepository를 상속 받은 것만으로도 빈이 생성되기 때문에 @Autowired를 통해 주입받고, @BeforeEach를 통해 매 Test 메소드가 실행 전에, 동작해야 하는 코드를 넣어주었다.

 

이번 글에서 예제를 위해 작업하는 프로젝트의 auto-ddl이 create-drop으로 설정되어 있기 때문에, 매 테스트마다 테이블을 새로 생성한다. 그렇기 때문에 테스트 실행전에 샘플 데이터를 저장하도록 하였다. 이 정도까지 하였으면, 메소드를 테스트하기 위한 사전 작업은 끝이다.

 

 

 

READ

CRUD의 R은 Read로, 데이터베이스에서 데이터를 조회하는 부분을 담당한다. Spring Data JPA의 JpaRepository는 Read를 위해 어떠한 기본 메소드를 제공하는지, 해당 메소드가 어떤 쿼리문을 통해서 작업을 하는지를 알아보자.

 

findAll()

findAll() 메소드는 테이블의 모든 데이터를 조회하는 기능이다. 

List<T> findAll();

 

반환 타입은 T 타입, 즉 JpaRepository를 상속 받을 때 지정해준 타입으로 된 리스트를 반환하는 것이다. 테스트 코드를 아래와 같이 작성하고 돌려보면,

@Test
void test() {
    List<Student> studentList = studentRepository.findAll();
    studentList.forEach(System.out::println);
}

 

아래와 같이 findAll을 실행하기 위해 JPA가 실행한 쿼리문을 볼 수 있고, 해당 결과 테이블에 저장된 데이터가 잘 출력된 것도 확인할 수 있다.

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
Student(id=1, name=kim, address=address1)
Student(id=2, name=lee, address=address2)
Student(id=3, name=park, address=address3)

 

findAll() + Sort

Sort 를 사용하여 findAll의 출력 값을 정렬하는 것도 가능하다. Sort에 대해서는 추후에 더 깊게 다룰 것이기 때문에 이번 예제에서는 Sort를 이용하여 ORDER BY 를 할 수 있다는 것 정도만 알고 넘어가겠다.

@Test
void test() {
    List<Student> studentList = studentRepository.findAll(Sort.by(Sort.Direction.DESC, "id"));
    studentList.forEach(System.out::println);
}

 

 

해당 코드를 동작시켜보면 위의 findAll 메소드의 쿼리문에서 order by 가 추가되고 id 칼럼이 내림차순으로 출력되는 것 또한 확인할 수 있다.

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
  order by
    s1_0.id desc
Student(id=3, name=park, address=address3)
Student(id=2, name=lee, address=address2)
Student(id=1, name=kim, address=address1)

 

findAll() + Pageable

이번에는 Pageable 구현체를 이용하여 findAll 한 결과를 Paging 하여 볼 수 있는 메소드이다. Pageable 구현체도 Sort와 마찬가지로 추후에 자세히 다루도록 하겠다.

@Test
void test() {
    Page<Student> studentPage = studentRepository.findAll(PageRequest.of(1,2));
    // Page 메소드들
    // getTotalElements() : 전체 데이터 개수
    // getTotalPages() : 전체 페이지 개수
    // getNumberOfElements() : 현재 페이지의 데이터 개수
    // getSize() : 현재 페이지 크기
    // getContent() : 현재 페이지에 담겨 있는 데이터들 => 반환값 : List<T>
    studentPage.getContent().forEach(System.out::println);
}

 

코드를 돌려보면, limit을 이용하는 것을 볼 수 있는데 ? 에는 findAll() 메소드에 넘겨준 Pageable 구현체를 통해 결정되게 된다. Page의 경우, 페이지 하나당 2개의 데이터를 가지고 1번째 페이지이기 때문에( 페이지는 0 페이지부터 시작) 마지막 3번 데이터만 출력되는 된다.

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
  limit
    ?,?
Student(id=3, name=park, address=address3)

 

findAll() + Example

Example 객체를 사용한 findAll 메소드이다. 데이터의 특정 문자열이 포함되는 경우 등을 찾는데에 유용하게 사용할 수 있는 메소드이다. Example.of에 Entity 타입을 가지는 객체를 넘겨주어야 그 객체의 값을 이용하여 matcher를 동작시킨다.

@Test
void test() {
    ExampleMatcher matcher = ExampleMatcher.matching()
        .withIgnorePaths("name")  // 해당 컬럼은 무시함 => matching 하지 않음
        .withMatcher("address", endsWith());  // address 필드의 끝 부분을 matching함
        
    //Example.of(probe, ExampleMatcher) <-- probe는 실제 Entity X
    Example<Student> example = Example.of(new Student("kim", "s2"), matcher);
         
    List<Student> studentPage = studentRepository.findAll(example);
    studentPage.forEach(System.out::println);
}

 

위의 코드는 address 칼럼의 값이 s2로 끝나는 데이터를 모두 찾는 코드이고, 이를 돌려보면 LIKE를 사용하여 아래와 같이 출력되는 것을 볼 수 있다.

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
  where
    s1_0.address like ? escape '\\'
Student(id=2, name=lee, address=address2)

 

findById(ID id)

findById는 JpaRepository를 상속 받을 때 지정해주었던 ID 타입의 값을 파라미터로 받아 그 값을 PK로 가지는 데이터를 찾아 Optional<T> 을 리턴한다. Optional의 T는 ID와 마찬가지로 JpaRepository를 상속 받을 때 지정해준 Entity 타입이다.

@Test
void test() {
    // .orElse : 해당 Id의 데이터가 없으면 null을 할당
    Student st = studentRepository.findById(2L).orElse(null);
    System.out.println(st);
}

 

Java의 Optional은 NPE(Null Pointer Exception)을 방지할 수 있는 객체로, orElse를 사용하면 Optional에 담긴 데이터가 null일 경우 다른 값으로 지정해줄 수 있다. ( 이번 예제에서는 null을 그대로 담음 )

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
  where
    s1_0.id=?
Student(id=2, name=lee, address=address2)

 

코드를 돌려보면, 위와 같이 where 절을 이용하여 id 조건을 통해 검색하는 일반적인 Sql문을 볼 수 있고, 결과도 잘 나온 것을 볼 수 있다.

 

findAllById(Iterable<ID> ids)

findAllByIds 메소드는 여러개의 id 값을 넘겨주어 해당 id 값을 가지고 있는 데이터들을 모두 찾는다. 매개변수로는 iterable 객체를 넘겨 받고 있는데 iterable은 Collection 객체의 상위 interface이다. 즉, Collection 객체를 넘겨주면 된다.

@Test
void test() {
    List<Long> ids = List.of(1L, 3L);
    List<Student> studentList = studentRepository.findAllById(ids);
    studentList.forEach(System.out::println);
}

 

코드를 돌려보면, 아래와 같이 where절에 in 연산자를 통해 넘겨준 id 값들에 포함된 데이터를 찾는 것을 확인할 수 있다.

Hibernate:
  select
    s1_0.id,
    s1_0.address,
    s1_0.name
  from
    Student s1_0
  where
    s1_0.id in (?,?)
Student(id=1, name=kim, address=address1)
Student(id=3, name=park, address=address3)

 

existsById(ID id)

existById는 매개변수로 넘겨준 id 값을 가지는 데이터가 존재하는지 아닌지를 boolean 값으로 넘겨주는 메소드이다.

@Test
void test() {
    boolean isExist = studentRepository.existsById(1L);
    System.out.println("Dose data(pk = 1) exist? " + isExist);
}

 

코드를 동작시켜보면, count()를 사용하여 해당 id 값을 가지는 데이터의 수를 확인하여 그 결과에 따라 true or false를 반환하는 것을 알 수 있다.

Hibernate:
  select
    count(*)
  from
    Student s1_0
  where
    s1_0.id=?
Dose data(pk = 1) exist? true

 

existsById 이외에도 Example 객체를 매개변수로 받는 exists 메소드도 있다. 해당 메소드도 Example에 매칭된 데이터가 존재하는지 아닌지를 boolean 값으로 반환해준다.

 

count()

마지막으로, count() 메소드이다. 메소드 이름에서 알 수 있듯이, 테이블에 존재하는 모든 데이터 갯수를 반환하는 메소드이다. 반환타입은 long 이다.

@Test
void test() {
    long cnt = studentRepository.count();
    System.out.println("The number of data in student table: " + cnt);
}

 

아래와 같이 count(*) 을 이용하여 갯수를 반환해주는 것을 볼 수 있다.

Hibernate:
  select
    count(*)
  from
    Student s1_0
The number of data in student table: 3
728x90

'ORM > Spring Data JPA' 카테고리의 다른 글

기본 CRUD - Create & Update  (0) 2024.01.16
기본 CRUD - DELETE  (1) 2024.01.16
JPA Repository - 개념  (0) 2024.01.15
Entity 개념 & 기본 사용법  (1) 2024.01.11
JPA 란  (1) 2024.01.05