본문 바로가기
IT/오픈소스

JPA - 스프링 연동

by 모띠 2024. 12. 24.

 

 

 

본 포스팅은 총 4편으로 구성되어 있습니다.

2024.01.30 - [IT/오픈소스] - JPA - 기본 개념

2024.12.23 - [IT/오픈소스] - JPA - 고급 매핑

2024.12.23 - [IT/오픈소스] - JPA - 다양한 쿼리방법

2024.12.24 - [IT/오픈소스] - JPA - 스프링 연동

 

 

JPA + 스프링 연동

순수 java에서 JPA를 사용하였기 때문에 트랜잭션과, 엔티티매니저 등을 직접 설정해 주었다.

하지만, 스프링 프레임워크에서 JPA를 연동한다면 더 간단히 사용가능하다.

 

<!-- 스프링 프레임워크 + JPA 연동 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>1.6.6</version>
</dependency>	

<!-- JPA표준과 하이버네이트 core / jpa-xx-api도 함께 내려받음 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.3.7.Final</version>
</dependency>

 

META-INF/persistence.xml은 필요 없음.

 

<!-- @Transactional이 붙은 곳에 트랜잭션을 적용 -->
<tx:annotation-driven/>	 

<!-- 트랜잭션 관리자를 등록
     일반적으로 org.springframework.jdbc.datasource.DataSourceTransactionManager를 트랜잭션 관리자로 사용하지만
     JPA를 사용하려면 org.springframework.orm.jpa.JpaTransactionManager를 트랜잭션 관리자로 등록
     이 트랜잭션 관리자는 DataSourceTransactionManager가 하던 역할도 수행하므로
     JPA 뿐만 아니라 JdbcTemplate, MyBatis와 함께 사용할 수 있음 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>    

<!-- @Repository 선언된 스프링빈에 발생한 예외를 스프링이 추상화한 예외로 변환 -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

<!-- 스프링에서 JPA를 사용하려면 LocalContainerEntityManagerFactoryBean를 스프링 빈으로 등록해야한다. 
		 스프링프레임워크가 제공하는 방식으로 엔티티 매니지팩토리를 등록. persistence.xml은 필요없음 -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <!-- datasource : 사용할 데이터소스 등록 -->
    <property name="dataSource" ref="dataSource"/>
    <!-- packageToScan : @Entity 탐색 시작 위치, @Entit가 붙은 클래스를 자동으로 검색하기 위한 시작점을 지정  -->
    <property name="packagesToScan" value="dto"/>
    <!-- persistenceUniName : 영속성 유닛 이름을 설정하며, 여기처럼 이름을 설정하지 않으면 default라는 이름의 영속성 유닛 생성 -->
    <!-- jpaVendorAdapter: 사용할 JPA 벤더를 설정 -->
    <property name="jpaVendorAdapter">
        <!-- 하이버네이트 구현체를 사용하므로 HibernateJpaVendorAdapter 등록 -->
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
    </property>
    <!-- jpaProperties를 사용해서 하이버네이트 구현체의 속성을 설정 -->
    <property name="jpaProperties"> <!-- 하이버네이트 상세 설정 -->
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop> <!-- 방언 -->
            <prop key="hibernate.show_sql">true</prop>                   <!-- SQL 보기 -->
            <prop key="hibernate.format_sql">true</prop>                 <!-- SQL 정렬해서 보기 -->
            <prop key="hibernate.use_sql_comments">true</prop>           <!-- SQL 코멘트 보기 -->
            <prop key="hibernate.id.new_generator_mappings">true</prop>  <!-- 새 버전의 ID 생성 옵션 -->
            <prop key="hibernate.hbm2ddl.auto">none</prop>             <!-- DDL 자동 생성 -->
        </props>
    </property>
 </bean>

 

 

repository

@Repository
public class MemberRepository {

    @PersistenceContext
    EntityManager em; // 순수 자바에서는 매니저 팩토리에서 엔티티매니저를 직접생성해야하지만, 스프링에서는 컨테이너가 제공.

    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(int id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }

    public List<Member> findByName(String name) {
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
            .setParameter("name", name)
            .getResultList();
    }

}

 

 

 


 

 

 

Spring Data JPA

 

스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트.

데이터접근계층을 개발할 때 지루하게 반복되는 CRUD 패턴을 세련된 방법으로 해결한다.

스프링데이터 JPA는 CRUD 처리를 위한 공통 인터페이스를 제공한다. (구현할 수 있는 공통적인 것을 미리 만들어놓았음)

해당 인터페이스만 작성하면 실행시점에 구현객체를 동적으로 생성해서 주입해 준다.

→ 결국, 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성하면 된다.

 

 

 

public class MemberRepostitory{
    @PersistenceContext
    EntityManager em;
    
    public void save(Member member) {..}
    public Member findById(int id) {..}
    public List<Member> findAll() {..}
    public List<Member> findByName(String name) {..}
    public Member findByIdAndAge(int id, int age) {..}
    public List<Member> findMemberCustom()( ..}
}

@Repository
public class TeamRepository {

    @PersistenceContext
    EntityManager em;

    public void save(Team team) {..}
    public Team findById(int id) {..}
    public List<Team> findAll() {..}
}
public interface MemberRepostitory extends JpaRepository<Member, Integer>{
	List<Member> findByName(String name);
	Member findByIdAndAge(int id, int age);
}

public interface TeamRepository extends JpaRepository<Team, Integer>{}

 

위처럼 일반적인 CRUD 메서드는 JpaRepository인터페이스에서 공통으로 제공하므로 상속하기만 하면 된다.

다만, findByName(…)처럼 직접 작성한 메서드는 미리 제공되어있지 않다.

하지만, 스프링데이터 JPA는 (매우 놀랍게도) 메서드 이름을 분석해서 다음 JPQL을 실행한다. 

“select m from Member m where username = :username”

 

 

스프링데이터 JPA를 사용하기 위해서는 아래의 설정이 필요하다.

 

pom.xml

<!-- spring-data-common도 함께받음 -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.7.18</version>
</dependency>

 

config.xml (java와 선택)

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:repositories base-package="jpa.repository" />

 

config.java (xml과 선택)

@Configuration
@EnableJpaRepositories(basePackages = "jpa.repository")
public class JpaConfig { .. }

 

 

 

공통 인터페이스 기능

주요 메서드

  • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다. 새로운 id라면 em.persist(), 이미 있는 id가 존재한다면 em.merge()
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 em.remove()를 호출한다.
  • findOne(ID) : 엔티티 하나를 조회한다. 내부에서 em.find()를 호출한다. 즉시 조회를 하여 객체를 전달하는 방식이다. 없으면 null. 사라짐 → findById 권장
  • findById(ID) : 엔티티 하나를 조회한다.
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 em.getReference()를 호출한다. lazy-loading을 통해 객체를 전달하는 방식이다. 없으면 예외발생
  • findAll(…): 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.

 

 

쿼리 메소드기능

스프링데이터 JPA의 쿼리 메소드는 아래 3가지기능을 제공한다.

 

  • 메소드 이름으로 쿼리 생성

스프링데이터 JPA는 메소드의 이름을 분석해서 JPQL로 생성하고 실행한다.

findByUsername(String username);

"select m from Member m where username = :username"

 

 

물론 정해진 규칙에 따라 메소드 이름을 지어야하니, 공식문서를 참고한다.

https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

 

 

 

  • JPA NamedQuery

스프링데이터 JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능을 제공한다.

@Entity
@NamedQuery(
	name="Member.findByUsername",
	query="select m from Member m where m.username = :username")
public class Member { .. }

 

Named 쿼리를 JPA에서 직접 호출

public class MemberRepository {
	public List<Member> findByUsername(String username){
		...
		List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
		.setParameter("username", "회원1").getResultList();		
	}
}

 

 

스프링 데이터 JPA를 통해 메소드 이름만으로 호출

도메인클래스 + .(점) + 메소드 이름 = Member.findByUsername 이름의 Named쿼리를 먼저 실행하고 없다면 이름생성 쿼리 실행

public interface MemberRepository extends JpaRepository<Member, Integer>{
	List<Member> findByUsername(@Param("username") String username);
}

 

 

  • @Query, 리포지토리 메소드에 쿼리 정의

@Query를 사용하여 리포지토리에 메소드에 직접 쿼리를 정의한다.

public interface MemberRepository extends JpaRepository<Member, Integer>{

	@Query("select m from Member m where m.username = ?1")
	List<Member> findByUsername(String username);
}

 

혹은, 네이티브 SQL을 사용할 수 도 있다.

public interface MemberRepository extends JpaRepository<Member, Integer>{

	@Query("select * from Member where username = ?0", nativeQuery = true) // 네이티브는 바인딩 0부터 시작
	List<Member> findByUsername(String username);
}

 

 

 

파라미터 바인딩

스프링데이터 JPA는 위치기반 파라미터 바인딩과, 이름기반 바인딩을 모두 지원한다.

select m from Member m where m.username = ?1 // 위치 기반
select m from Member m where m.username = :username // 이름 기반 (권장)

public interface MemberRepository extends JpaRepository<Member, Integer>{

	@Query("select * from Member where username = :name") 
	List<Member> findByUsername(@Param("name") String username);
}

 

 

벌크성 수정 쿼리

스프링데이터 JPA에서도 벌크성 수정 쿼리를 작성할 수 있다.

다만, 벌크성 쿼리를 실행하고나서 영속성 컨텍스트를 초기화하고싶으면 @Modifying(clearAutomatically = true) 옵션을 줘야한다. (디폴트false)

// JPA로 직접 작성
em.create("update Product p set p.price = p.price * 2").executeUpdate();

// 스프링 데이터 JPA로 작성
@Modifying
@Query("update Product p set p.price = p.price * 2)
int bulkPriceUp();

 

 

페이징 정렬

스프링데이터 JPA는 쿼리메소드에 페이징과 정렬기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.

  • Sort : 정렬
  • Pagable : 페이징 (내부에 Sort포함)

파라미터로 Pageable을 사용하면 Page / List를 리턴받을 수 있는데,

Page를 사용하면 스프링데이터JPA는 페이징기능을 제공하기위해 검색된 전체 데이터 건수를 조회하는 Count쿼리를 알아서 실행한다.

// count쿼리 사용
Page<Member> findByName(String name, Pageable pageable);

// count쿼리 미사용
List<Member> findByName(String name, Pageable pageable);

Page<Member> findByName(String name, Sort sort);
// Pageable인터페이스의 구현체
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC), "name");

Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);

List<Member> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부

 

 

 

사용자 정의 리포지토리 구현

스프링데이터 JPA로 리포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다.

하지만 다양한 이유로 메소드를 직접 구현해야 할때도 있다.

공통 인터페이스가 제공하는 기능을 모두 구현하지 않으면서 필요한 메소드만 구현 가능하다.

 

사용자 정의 인터페이스 작성

public interface MemberRepositoryCustom {
	public List<Member> findMemberCustom();
}

 

“리포지토리 인터페이스명 + Impl” 이라는 명명 규칙을 지켜야한다.

public class MemberRepositoryImpl implements MemberRepositoryCustom  {
	@Override
	public List<Member> findMemberCustom() { ...구현....}
}

 

사용자 정의 인터페이스를 상속

public interface MemberRepository extends JpaRepository<Member, Integer>, MemberRepositoryCustom {}

 

 

 

 

스프링 데이터 JPA + QueryDSL

스프링데이터JPA는 2가지 방법으로 QueryDSL을 지원한다.

 

  • QueryDslPredicateExecutor

간단한 기능은 사용가능하지만, join 등의 한계가 존재

public interface ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> { .. }


// 장난감이라는 이름을 포함하면서 가격이 1000~2000사이인 상품을 검색
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(item.name.contains("장난감").and(item.price.between(1000,2000));

 

 

  • QueryDslRepositorySupport

QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용해야한다.

하지만, 스프링데이터JPA가 제공하는 QueryDslRepositorySupport를 상속받으면 편하게 사용할 수 있다.

 

public interface TeamRepositoryCustom {

    public List<Team> findByDynamicQuery(Integer age, String username);
}
public class TeamRepositoryImpl extends QueryDslRepositorySupport implements TeamRepositoryCustom {

    public TeamRepositoryImpl() {
        super(Team.class);
    }

    @Override
    public List<Team> findByDynamicQuery(Integer age, String username) {
        QTeam team = QTeam.team;
        QMember member = QMember.member;

        JPQLQuery query =
            from(team) // TeamRepositoryImpl.this.from(team) 동일
                .join(team.member, member);

        if (age != null) {
            query.where(member.age.eq(age));
        }

        if (username != null) {
            query.where(member.username.eq(username));
        }

        return query.list(team);
    }
}
public interface TeamRepository extends JpaRepository<Team, Integer>, TeamRepositoryCustom {
    public List<Team> findByName(String name);
}

 

 

 

그러나, 스프링데이터 JPA 2.x버전부터는 QueryDslRepositorySupport를 공식적으로 지원하지않는다.

QueryDslRepositorySupport를 직접 구현하여 JPAQueryFactory를 사용하는것을 권장.

package repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import com.querydsl.jpa.impl.JPAQueryFactory;

public abstract class QuerydslSupport {

    @PersistenceContext
    private EntityManager entityManager;

    private JPAQueryFactory queryFactory;

    protected JPAQueryFactory getQueryFactory() {
        if (queryFactory == null) {
            queryFactory = new JPAQueryFactory(entityManager);
        }
        return queryFactory;
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }
}

......................

return getQueryFactory().selectFrom(team)
    .join(team.member, member) // team.member와 member를 조인
    .where(
        eqAge(age),
        eqUsername(username) // 조건 동적 추가
    )
    .fetch();

 

 

미리 구현해놓기 싫다면 JPAQueryFactory를 직접 사용해도 된다. 

package repository;

import java.util.List;

import javax.persistence.EntityManager;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

import dto.QMember;
import dto.QTeam;
import dto.Team;

public class TeamRepositoryImpl implements TeamRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public TeamRepositoryImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Override
    public List<Team> findByDynamicQuery(Integer age, String username) {
        QTeam team = QTeam.team;
        QMember member = QMember.member;

        return queryFactory.selectFrom(team)
            .join(team.member, member) // team.member와 member를 조인
            .where(
                eqAge(age),
                eqUsername(username) // 조건 동적 추가
            )
            .fetch();
    }

    // 조건 메서드로 분리
    private BooleanExpression eqAge(Integer age) {
        return age != null ? QMember.member.age.eq(age) : null;
    }

    private BooleanExpression eqUsername(String username) {
        return username != null ? QMember.member.username.eq(username) : null;
    }
}

 

 

 

댓글