Mybatis 버전의 방명록 애플리케이션( 링크 )을 JPA 버전으로 만드는 주제입니다.





Spring Data JPA

Spring Data JPA를 알아보기 전에, 먼저 JPA의 큰 흐름을 살펴보겠습니다.


JPA는 JDBC( Mybatis )개발 시 DAO의 반복적인 CURD 작업 해결 및 객체를 데이터 전달 목적이 아닌 객체 지향적인 개발 방법을 사용하려는 목적에서 개발되었습니다. 


그런데 JPA가 제공해주는 기본적인 메서드만으로 복잡한 검색이 불가능했고, 그래서 SQL과 유사한 JPQL이 도입되었습니다.

그러다가 JPQL 작성 시 문법적 오류가 발생할 수 있고, 좀 더 높은 생산성을 위해 메서드 호출로 쿼리를 생성하는 QueryDSL을 사용하게 되었습니다.


JPQL과 QueryDSL을 사용해도 충분히 JPA를 잘 사용하고 있는 것입니다.

그런데 JPA에서 Repository 계층을 구현하다 보면 간단한 CURD 작업을 반복적으로 작성하게 됩니다.

객체지향 개발 방법에서 반복적인 작업은 피하는 것이 좋으므로 이를 해결 할 수 있는 방법을 생각했는데, 이것이 Spring Data JPA입니다.



Spring Data JPA는 Repository 계층의 반복되는 작업을 피하기 위해 JpaRepository 인터페이스를 제공합니다.

JpaRepository 인터페이스에는 자주 사용되는 기본적인 CRUD 메서드가 선언되어 있는데, 개발자는 Repository 클래스를 생성하지 않고 인터페이스를 생성하여 JpaRepository 인터페이스를 상속받으면 JpaRepository에서 선언한 메서드들을 사용 할 수 있게 됩니다.

뿐만 아니라 JpaRepository에서 제공하는 메서드 외에 필요한 쿼리를 수행하기 위한 메서드를 직접 정의할 수도 있습니다.



우선 지금까지 JPQL , QueryDSL로 만들었던 방명록 애플리케이션을 Spring Data JPA 버전으로 수정해보도록 하겠습니다.

구체적인 환경 설정은 여기를 참고해주시고, 이 글에서는 Spring Data JPA을 사용하는데 필요한 환경 설정만 언급하도록 하겠습니다.





환경 설정

pom.xml

<!-- 스프링 데이터 JPA -->

<dependency>

         <groupId>org.springframework.data</groupId>

         <artifactId>spring-data-jpa</artifactId>

         <version>1.8.0.RELEASE</version>

</dependency>



applicationContext.xml

<beans

xmlns:jpa="http://www.springframework.org/schema/data/jpa"

xsi:schemaLocation

http://www.springframework.org/schema/data/jpa

http://www.springframework.org/schema/data/jpa/spring-jpa.xsd

>


<!-- JPA repository Interface Scanning -->

<jpa:repositories base-package="com.victolee.guestbook.repository" />


Spring data JPA를 사용하기 위한 설정입니다.


애플리케이션이 실행되면 JpaRepository를 상속받은 인터페이스를 Spring Data JPA가 구현하게 되는데, 구현해야 할 인터페이스를 스캔 할 수 있도록 스캔 범위를 작성해야 합니다.





GuestbookRepository 인터페이스 생성

public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{

}

GuestbookRepository 인터페이스를 생성하고 JpaRepository 인터페이스를 상속 받도록 합니다.

제네릭 타입에서 두 번째인 Integer는 @Id의 타입을 의미합니다.


GuestbookRepository는 JpaRepository 인터페이스만 상속 받으면 애플리케이션이 실행될 때 JpaRepository로부터 상속 받은 메서드를 Spring data JPA가 구현해주기 때문에, JpaRepository의 메서드를 사용할 수 있습니다.



Repository 계층에서 해야 할 일은 이것이 끝입니다.




GuestbookService 수정

@Service
@Transactional 
public class GuestbookService {
	@Autowired
	private GuestbookRepository guestbookRepository;
	
	public List<Guestbook> getMessageList(){
		return guestbookRepository.findAll();
	}
	
	public void insertMessage(Guestbook guestbook) {
		guestbook.setRegDate(new Date());
		guestbookRepository.save(guestbook);
	}
	
	public void deleteMessage(Guestbook guestbook) {
		guestbookRepository.delete(guestbook);
	}
}

findAll() , save() , delete() 메서드가 바로 JpaRepository 인터페이스에 선언되어 있는 메서드입니다.

GuestbookRepository 인터페이스는 JpaRepository 인터페이스를 상속 받았으므로 이 메서드들을 갖고 있으며,

메서드를 호출하기만 하면 애플리케이션이 실행될 때 메서드가 구현되므로 실제로 쿼리가 생성됩니다.




이것으로 방명록 애플리케이션을 Spring Data JPA 버전으로 바꿔보았습니다.

인터페이스를 생성하고 상속 받기만 했는데 Repository 개발이 끝났습니다.

생산성이 어마 어마 하다는 것을 느끼셨을 겁니다.


이제 할 일은 Spring Data JPA가 제공하는 메서드가 어떤 것들이 있는지 알아보는 것입니다.





Spring Data JPA가 제공하는 메서드

기본 메서드

save() : 새로운 엔티티는 저장( persist )하고 이미 있는 엔티티는 수정합니다.

( 수정된 필드만 수정 되는 것이 아니라 모든 필드에 대해서 업데이트가 진행됩니다. )

delete() : 엔티티 하나를 삭제합니다.

findOne() : 엔티티 하나를 조회합니다

getOne() : 엔티티가 아닌 프록시로 조회합니다.

findAll() : 모든 엔티티를 조회합니다.



JpaRepository 인터페이스를 상속 받은 GuestbookRepository 인터페이스가 호출 할 수 있는 메서드 목록입니다.

CRUD와 관련된 여러 메서드를 제공하고 있는데, 모든 메서드가 JpaRepository에서 제공하는 것은 아닙니다.


JpaRepository의 상속 관계는 다음과 같습니다.



이 밖에 Spring Data JPA와 관련된 API가 더 궁금하시다면 여기를 참고해주세요. 





그런데 메서드 목록을 보시면, 복잡한 쿼리를 수행할 수 없을 것 같은 느낌이 듭니다.

가장 기본이 되는 조건( where 절 ) 을 추가하거나 정렬( order )하고 싶으면 어떻게 해야 할까요?


이를 해결하기 위해 Spring Data JPA는 몇 가지 방법을 제공해주지만, 이 글에서는 2가지 방법에 대해서 알아보고자 합니다.

1) 메서드 이름으로 쿼리 생성

2) @Query 어노테이션




1. 쿼리 메서드 - 메서드 이름으로 쿼리 생성

메서드 이름으로 쿼리 생성이란, 규칙에 맞게 메서드 이름을 “ 잘 “ 정의한다면 Spring Data JPA가 메서드 이름을 파싱하여 쿼리를 생성해내는 방식입니다.


GuestbookRepository

public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{
	List<Guestbook> findAllByOrderByRegDateDesc ();
}

findAllByOrderByRegDateDesc 라는 이름의 메서드를 선언했습니다.

인터페이스이기 때문에 메서드 몸통을 작성할 필요가 없습니다.

이렇게 메서드 헤더를 선언만 하면 Spring Data JPA가 이 메서드 이름을 “ findAll , orderby , regDate , desc “으로 파싱하여 쿼리를 만들어냅니다.


GuestbookService에서 findAllByOrderByRegDateDesc() 메서드를 호출하면 다음과 같은 쿼리가 출력됩니다.




예제를 하나 더 살펴보겠습니다.

// GuestbookRepository
public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{
	List<Guestbook> findByName(String name);
}


// GuestbookService
public List<Guestbook> getMessageList(){
	String name = "victolee";
	return guestbookRepository.findByName(name);
}

victolee라는 이름의 사용자가 쓴 방명록만 조회하는 코드입니다.

Service 계층으로부터 매개변수를 받을 수도 있다는 것을 확인할 수 있습니다.


이 메서드를 실행하면 아래와 같은 결과 쿼리가 출력됩니다.




어느 정도 느낌이 오셨나요?

이렇게 Repository에서 메서드 이름만 잘 작성하면 쿼리가 수행됩니다.


그 밖에 메서드 이름 규칙에 대해서는 모두 다룰 수 없으므로 여기를 참고해주세요.





2. 쿼리 메서드 - @Query 어노테이션

@Query 어노테이션 방법은 쿼리를 직접 작성하는 방식입니다.

기본 값으로 JPQL을 작성할 수 있지만, 속성 값 변경을 통해 SQL을 작성할 수도 있습니다.


예제로 JpaRepository에서 제공하는 findAll() 메서드, 즉 모든 방명록을 조회하도록 @Query 어노테이션을 작성해보겠습니다.

// GuestbookRepository
public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{
	@Query("SELECT g FROM Guestbook g")
	List<Guestbook> foo();
}


// GuestbookService
public List<Guestbook> getMessageList(){
	return guestbookRepository.foo();
}

보시는 바와 같이 @Query 어노테이션에 JPQL을 작성함으로써 원하는 쿼리를 수행할 수 있습니다.

메서드 이름을 foo라고 한 것은 메서드 이름이 쿼리에 영향을 주지 않는다는 것을 보여주기 위함입니다.


조건문과 정렬을 하는 방법은 JPQL을 잘 알고 있다면 문제될 것이 없습니다.





@Modifying

방명록 애플리케이션에는 수정이 없지만 수정과 관련하여 @Query를 사용할 때 언급하고 싶은 것이 있습니다.

User라는 엔티티가 존재하여 회원 정보를 수정한다고 가정해보겠습니다.

//UserRepository
import org.springframework.data.repository.query.Param;

public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{
	@Modifying	// update , delete Query시 @Modifying 어노테이션을 추가
	@Query(value="UPDATE User u SET u.name = :name WHERE u.no = :no", nativeQuery=false)
	Integer update(@Param("name") String name,
					@Param("no") Integer no);
}


// UserService
public void updateUserInfo(User user) {
	userRepository.update( user.getName(), user.getNo() );
}

@Query 어노테이션을 사용해서 엔티티를 수정하거나 삭제할 때는 @Modifying 어노테이션이 필요합니다.

그리고 JPQL에서 알아보았던 이름 기준 파라미터 바인딩을 사용했습니다.


@Query 어노테이션의 쿼리를 JPQL이 아닌 SQL로 바꾸고 싶다면 nativeQuery=true로 변경하면 됩니다.

기본 값은 false입니다.



그런데 User 엔티티의 칼럼( getName() , getNo() )을 각각 전달하는 방법보다 User 객체를 통째로 넘겨주는 것이 효율적입니다.
그러기 위해서는 다음과 같이 수정하면 됩니다.
// UserRepository
public interface GuestbookRepository extends JpaRepository<Guestbook, Integer>{
	@Modifying	// update , delete Query
	@Query(value="update User u set u.name = :#{#user.name} WHERE u.no = :#{#user.no}", nativeQuery=false)
	Integer update(@Param("user") User user );
}


// UserService
public void updateUserInfo(User user) {
	userRepository.update( user );
}

객체를 통째로 전달하는 이 방식을 더 많이 사용합니다.




이상으로 JpaRepository가 제공하는 메서드 외에 필요한 쿼리를 더 작성하고 싶은 경우, 사용 할 수 있는 2가지 방법을 알아보았습니다.


다음으로는 방명록 애플리케이션 주제와 관련은 없지만, Spring Data JPA는 페이징 기능을 제공합니다.

유용한 기능이라 언급을 하려 합니다.




페이징

클라이언트가 게시판( Board )의 페이지 번호를 누르면, 컨트롤러에서 페이지 번호를 받아 service 계층에 넘겨줄 것입니다.

이 때 페이지 번호를 pageNum이라 하고, Board 엔티티가 존재한다고 가정하겠습니다.

// BoardRepository
public interface BoardRepository extends JpaRepository<Board, Integer>{
	@Query("select b from Board b order by b.regDate desc")
	Page<Board> findAllPaging(Pageable pageable);
}


// BoardService
public Page<Board> getList(Integer pageNum){
	PageRequest pageRequest
						 = new PageRequest(pageNum - 1 , 5, new Sort(Sort.Direction.DESC, "regDate"));
	Page<Board> result = BoardRepository.findAllPaging(pageRequest);
	
	return result;
}


// BoardController
public String list(
	Page<Board> pagingResult = boardService.getList(pageNum);
}

BoardService의 getList() 메서드에서 PageRequestPageable 인터페이스를 구현한 구현체입니다.


PageRequest()의 생성자로

첫 번째는 offset을 의미합니다.

즉 pageNum의 값이 3이라면, 3-1 = 2라는 값이 인자로 전달되며, 따라서 10개의 엔티티를 건너 뛴 후 엔티티가 조회됩니다.


두 번째는 조회하는 엔티티 개수를 의미합니다..

즉 한 페이지에는 5개의 게시글이 존재합니다.


세 번째는 정렬 방식을 정의합니다.

날짜를 기준으로 내림차순으로 정렬했습니다.



페이징에 의해 게시글이 조회될 것이고, 그 결과는 컨트롤러의 pagingResult 객체에 담길 것입니다.

컨트롤러에서는 pagingResult를 통해 페이징에 대한 다양한 정보를 사용할 수 있습니다. 



getContent() 메서드는 데이터 목록, 즉 페이징된 게시글의 목록 List로 반환하고,

nextPageable() 메서드는 다음 페이지가 존재하는지 반환합니다.


List<Board> 타입으로 페이징된 데이터를 직접 받을 수 있지만 Page<T> 인터페이스는 페이징과 관련된 여러 메서드를 호출할 수 있습니다.

여러 메서드의 역할은 API 문서를 참고하시길 바랍니다. ( 링크 )





이상으로 Spring Data JPA에 대해 알아보았습니다.

정리하면,

JpaRepository 인터페이스에는 자주 사용하는 쿼리를 수행하는 메서드를 선언해 놓았습니다.

따라서 DB에 접근하는 Repository는 인터페이스로 정의하여 JpaRepository를 상속받기만 하면, 애플리케이션이 실행 될 때 Spring Data JPA가 JpaRepository의 메서드를 구현합니다.


그런데 JpaRepository가 제공하는 메서드만으로 복잡한 쿼리를 수행할 수 없습니다.

이를 해결 하기 위해, 메서드 이름을 규칙에 맞게 작성하면 메서드 이름을 파싱하여 쿼리로 바꿔주는 방식과 @Query 어노테이션을 통해 JPQL을 작성하는 방식을 제공합니다.


Spring Data JPA는 실무에서 사용하는 방법이므로 꼭 알아야 하는 기술입니다.
더 자세한 사용 방법은 Spring Data JPA 공식 문서를 참고하시길 바랍니다 !


댓글 펼치기 👇
  1. Daxhoont 2019.12.26 15:35

    정말 감사합니다!!
    jpa관련해서 공부중이었는데 너무 정리가 잘 되어있어서 이해가 쑥쑥 들어왔습니다.
    이참에 다른 글도 봐서 제 지식을 늘려야 겠습니다.
    다시 한번 정말 감사합니다!!

  2. SSO SSO 2020.03.05 15:05

    정말 큰 도움이 되었습니다.

    체계적으로 예제를 잘 넣어주셔서 초보자도 정말 보기가 좋았습니다.!!

    개념정리가 잘되었습니다. 최고!

  3. 동숲이 2020.05.13 15:58

    JPA 관련하여 본 글 중에 가장 정리가 잘 되어 있다고 느꼈습니다.
    덕분에 많은 도움이 되었습니다.
    감사합니다.