본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - Spring Data JPA

by 방구석 대학생 2021. 11. 16.

"인프런의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확인해주세

www.inflearn.com

Spring Data JPA

스프링 부트와 JPA 만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어든다.

여기서 스프링 데이터 JPA 를 사용하면 기존의 한계를 넘어 마치 마법과도 같이 레포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.

그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA 가 모두 제공한다.

지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어드는 것이다.

따라서 개발자는 핵심 비즈니스 로직을 개발하는데 집중할 수 있게 된다.

 

* 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA 는 선택이 아니라 필수이다.

* 스프링 데이터 JPA 는 JPA 를 편리하게 사용하도록 도와주는 기술이다. 따라서 JPA 를 먼저 학습한 후에 스프링 데이터 JPA 를 학습해야 한다.

(JPA 에 대한 학습의 경우 기술 블로그에 강의 내용을 정리한 글 들을 참조하자. - 글의 갯수가 무려 66개나 된다.)

 

Spring Data JPA 의 경우 인터페이스를 만들어야 한다.

다음과 같은 인터페이스를 만들고 코드를 작성한다.

- - SpringDataJpaMemberRepository.java

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Override
    Optional<Member> findByName(String name); // 뭔가 코드를 구현할 것 없이 이 코드 한 줄만으로 끝이다.
}

* 인터페이스의 경우 상속을 받을때 클래스와 마찬가지로 extends 키워드를 활용하며, 다중 상속이 가능하다.

* JpaRepository 인터페이스를 상속받고 있으면 Spring Data JPA 가 자동으로 구현체를 만들어줌과 동시에 해당 인터페이스를 스프링 빈에 등록해준다.(중요!!!!!)

 

위와 같이 인터페이스를 작성해주고 SpringConfig 클래스를 아래와 같이 수정해주자.

 - SpringConfig.java

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //    private DataSource dataSource;
//    private EntityManager em; // JPA 를 위한 EntityManager 객체 필드 선언

//    @Autowired
//    public SpringConfig(DataSource dataSource) {
//        this.dataSource = dataSource;
//    }

//    @Autowired
//    public SpringConfig(EntityManager em){ // 생성자를 통해 EntityManager 의존성을 SpringConfig 객체에 주입해준다.
//        this.em = em;
//    }

//    @Bean
//    public MemberService memberService(){
//        return new MemberService(memberRepository());
//    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository);
    }

//    @Bean
//    public MemberRepository memberRepository(){
////        return new MemoryMemberRepository();
////        return new JdbcMemberRepository(dataSource);
////        return new JdbcTemplateMemberRepository(dataSource);
////        return new JpaMemberRepository(em); // JPAMemberRepository 를 스프링 빈으로 등록한다.
//    }
}

 

위의 코드를 보면 그냥 단순히 MemberRepository 객체를 선언하고, 그에 대한 생성자를 통해 SpringConfig 클래스에 MemberRepository 객체를 의존성 주입 받고 있다고 볼 수도 있다.

그런데 이렇게 작성해둬도 위에서 작성한 SpringDataJpaMemberRepository 객체가 자동으로 MemberRepository 객체에 적재되면서 SpringConfig 객체에 의존성으로서 주입받을 수 있게 된다.

 

어떻게 이것이 가능하느냐, 위의 SpringDataJpaMemberRepository 인터페이스에서 설명했듯이 JpaRepository 인터페이스를 상속받고 있는 인터페이스는 Spring Data JPA 에서 자동으로 스프링 빈으로서 객체를 등록해주기 때문이다.

거기에 MemberRepository 인터페이스 또한 같이 상속 받고 있으므로 MemberRepository 인터페이스가 해당 인터페이스 객체의 인스턴스 역할을 하게 됨으로서(뇌피셜), SpringConfig 클래스에서 MemberRepository 객체를 만들었음에도 불구하고 스프링에서 스프링 빈으로 등록되어 있는 SpringDataJpaMemberRepository 객체를 매칭시켜서 적재해주는 것이 아닐까 싶다.

 

그 결과 SpringConfig 에서 MemberRepository 객체에 정상적으로 생성자를 통해 SpringDataJpaMemberRepository 객체를 의존성 주입받게 되고, 주입받은 해당 객체를 통해 MemberService 객체를 생성하면서 스프링 빈으로 등록하는 것 또한 가능해진다.

(JpaRepository 인터페이스를 상속받는 순간 SpringDataJpaMemberRepository 인터페이스가 스프링 빈으로 등록 되었으므로, MemberRepository 객체를 스프링 빈으로 등록하는 메소드인 memberRepository() 는 필요없게 되어 주석 처리하였다.)

 

이제 회원가입 테스트 코드를 동작시켜 보면 테스트가 정상적으로 잘 통괴되는 것을 확인할 수 있다.

(물론 h2 데이터베이스는 기본적으로 켜놓은 상태여야 한다.)

테스트 결과로 hibernate 에서 생성해준 SQL 쿼리 로그 또한 앞전에 스프링 데이터 JPA 를 사용하지 않고 일반적인 JPA 를 사용했을 때와 똑같은 내용이 출력된것을 확인 할 수 있다.

- SQL 로그

Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_ where member0_.name=?
Hibernate: insert into member (id, name) values (null, ?)

물론 중복_회원_예외 메소드 까지 같이 합쳐서 코드 전체를 동작시켜 봐도 테스트가 정상적으로 잘 통과된다.

 

 

의문점?

그렇다면 기존에 MemberRepository 인터페이스에 작성해 두었던 save, findById, findAll 과 같은 메소드들은 다 어디로 갔을까?

SpringDataJpaMemberRepository 인터페이스가 상속 받고 있는 JpaRepository 의 상세한 코드 내용을 뜯어보자.

- JpaRepository.java

@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll()
	 */
	@Override
	List<T> findAll();

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
	 */
	@Override
	List<T> findAll(Sort sort);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
	 */
	@Override
	List<T> findAllById(Iterable<ID> ids);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
	 */
	@Override
	<S extends T> List<S> saveAll(Iterable<S> entities);

	/**
	 * Flushes all pending changes to the database.
	 */
	void flush();
	
	// .........
	// 이하 코드 생략
}

위의 코드를 보면 JpaRepository 인터페이스에서 자체적으로 제공해주는 findAll 과 같은 메소드가 있음을 확인 할 수 있다.(물론 그 외에 다른 메소드들도 많다.)

그렇다면 이번엔 JpaRepository 인터페이스가 상속받고 있는 PagingAndSortingRepository 인터페이스의 코드도 한번 뜯어보자.

- PagingAndSortingRepository.java

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

	/**
	 * Returns all entities sorted by the given options.
	 *
	 * @param sort
	 * @return all entities sorted by the given options
	 */
	Iterable<T> findAll(Sort sort);

	/**
	 * Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
	 *
	 * @param pageable
	 * @return a page of entities
	 */
	Page<T> findAll(Pageable pageable);
}

위의 코드를 봤을 땐 자세히는 모르겠지만, 강사님 말씀으로는 페이징 처리를 해주는 인터페이스 라고 한다.

이번엔 CrudRepository 인터페이스의 코드를 한번 뜯어보자.

- CrudRepository.java

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	/**
	 * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
	 * entity instance completely.
	 *
	 * @param entity must not be {@literal null}.
	 * @return the saved entity; will never be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
	 */
	<S extends T> S save(S entity);

	/**
	 * Saves all given entities.
	 *
	 * @param entities must not be {@literal null} nor must it contain {@literal null}.
	 * @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
	 *         as the {@literal Iterable} passed as an argument.
	 * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
	 *           {@literal null}.
	 */
	<S extends T> Iterable<S> saveAll(Iterable<S> entities);

	/**
	 * Retrieves an entity by its id.
	 *
	 * @param id must not be {@literal null}.
	 * @return the entity with the given id or {@literal Optional#empty()} if none found.
	 * @throws IllegalArgumentException if {@literal id} is {@literal null}.
	 */
	Optional<T> findById(ID id);

	// .......
	// 이하 코드 생략
}

 

위의 코드를 보면 save 메소드와 findById 메소드가 선언되어 있음을 알 수 있다.

즉, SpringDataJpaMemberRepository 인터페이스에서 상속받는 각종 상위 인터페이스에서 우리가 앞서 MemberRepository 에 선언해 두었던 save, findById, findAll 과 같은 기본적인 공통 데이터베이스 통신 메소드들을 제공해주고 있기 때문에, 상위 인터페이스들이 제공해주지 않는 findByName 과 같은 메소드를 제외하고는 굳이 SpringDataJpaMemberRepository 인터페이스에서 선언해줄 필요가 없게 되는 것이다.

- findByName 과 같이 비즈니스 내용에 따라 서로 공통될 수 없는 메소드들은 따로 작성해주어야 한다.

Spring Data JPA 인터페이스 구조

 

 

여기서 또 다시 궁금한 점?

아무리 그렇다고 해도 SpringDataJpaMemberRepository 인터페이스에 고작 findByName 이라는 메소드 하나만 선언해두고, 해당 메소드를 어떻게 동작시키는지 에 대한 구현 코드는 단 한 줄도 작성되어 있지 않은데

어떻게 이 메소드가 마치 구현 코드라도 있는것 처럼 정상적으로 동작하는 걸까?

 

 * JpaRepository 인터페이스를 상속받는 인터페이스에 작성된 메소드에 적용되는 규칙

- 예를 들어 우리가 작성해놓은 findByName 과 같이 메소드의 이름을 정의해주면 어떤일이 벌어지느냐,

- JPA 에서 메소드의 이름에 따라 JPQL 쿼리를 다음과 같이 작성해준다.

- select m from Member m where m.name = ?

- 이와 같이 JPQL 이 작성되고 나면 이를 SQL 로 번역하여 데이터베이스에 전달해준다.

 

위의 내용을 좀 더 자세히 설명하자면, findBy + Name 이라는 키워드 조합만으로 JPA 에서 그에 맞는 JPQL 쿼리를 작성해주는 것이다.(메소드 이름 만으로 조회 기능을 제공한다.)

여기서 만약 검색조건을 하나 더 추가해주고 싶다면 findByNameAndId 와 같이 메소드 이름을 정의해주고, 메소드의 파라미터로 id 값을 넘겨주면 JPQL 로 작성되는 쿼리에서 where 조건에 name 과 더불어 id 값 또한 추가해 줄 수 있게 된다.

 

메소드의 이름을 규칙에 맞게 작성해주는 것으로 필요한 모든 코드 구현이 끝난다.

이게 바로 Spring Data JPA 가 부리는 마법이다.

 

* 참고

실무에서는 JPA 와 스프링 데이터 JPA 를 기본적으로 사용하고, 복잡한 동적 쿼리는 Querydsl 이라는 라이브러리를 사용하면 된다.

Quertdsl 을 사용하면 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다.

(Mybatis 에 비해 JPA 가 가지고 있는 단점을 보완해준다.)

이 조합으로 해결하기 어려운 쿼리는 JPA 가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate 을 사용하면 된다.