Spring Data JPA

떡ol·2023년 8월 4일

들어가기

JPA를 통해 다양한 Entity의 CRUD를 매번 작성하는게 번거롭습니다. 이러한 반복적인 구현체를 빠르게 구현가능하며, 편의 매소드(Paging, Auditing)도 제공해줍니다. 나중에 Hibernate의 버전이 바뀌거나 다른 구현체로 바꾸고 싶을때도 교체작업도 쉽습니다.

학습용 Entity 만들기

연습용으로 만들 Entity를 구성해봅시다. 저는 기존에 쓰던 프로젝트에 덮어서 사용하느라 파일이름 뒤에 Spring을 붙혀 사용하고 있습니다.

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class MemberSpring extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private TeamSpring team;
    public MemberSpring(String username) {
        this(username, 0);
    }
    public MemberSpring(String username, int age) {
        this(username, age, null);
    }
    public MemberSpring(String username, int age, TeamSpring team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }
    public void changeTeam(TeamSpring team) {
        this.team = team;
        team.getMembers().add(this);
    }
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class TeamSpring {
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team")
    List<MemberSpring> members = new ArrayList<>();
    public TeamSpring(String name) {
        this.name = name;
    }
}

Spring Data JPA 사용하기

인터페이스에 JpaRepository를 확장하여 사용하시면 됩니다. 첫번째 타입값은 Entity이며, 두번째는 @Id(PK)의 타입을 작성해주시면 됩니다.

public interface MemberSpringRepository extends JpaRepository<MemberSpring, Long> 

쿼리 메소드 사용하기

이제 Spring은 특정규칙에 의해서 자동으로 CRUD에 해당하는 쿼리를 만들어줍니다.
규칙이란 메소드의 이름이며 다음의 사이트의 규칙에 따르게 됩니다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation

문서에 메소드 명명 규칙은 자세히 나와있지만, 간단하게 설명드리면
find를 통해 select절이라는것을 정하고 By를통해 WHERE절로 어떤 Param을 넘길 것인지를 이야기하고 있습니다.

List<MemberSpring> findAll(Sort sort); //전체조회
List<MemberSpring> findById(Long id); //아이디로 검색
List<MemberSpring> findByUsername(String username);
List<MemberSpring> findByUsernameAndAgeGreaterThan(String username, int age)

또한 반환 타입도 다양하게 지원합니다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

List<MemberSpring> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<MemberSpring> findByUsername(String name); //단건 Optiona

NamedQuery 사용하기

JPA는 기본적으로 Entity를 @NamedQuery를 이용하여 맵핑가능합니다.

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

@Query에서 맵핑이 가능합니다.

//기본 JPA 문법
em.createNamedQuery("MemberSpring.findByUsername", MemberSpring.class)
//Speing Data
@Query(name = "MemberSpring.findByUsername")
List<Member> findByUsername(@Param("username") String username);

선언시에 name ='MemberSpring.findByUsername' 이므로 JpaRepository 인터페이스의 구현체 매소드와 이름이 일치합니다. 따라서 Entity의 @namedQuery로 선언되어있는 것을 우선시 작동합니다.

//@Query 를 작성안하여도 Entity의 @NamedQuery와 interface의 매소드 이름이 findByUsername로 같습니다. 
//따라서 해당 매소드는 Entity꺼를 불러와 사용합니다.
//없을시에는 Spring Data에 의해서 '기본 문법'을 실행합니다.
List<MemberSpring> findByUsername(@Param("username") String username);

@Query, 값, DTO 조회하기

@Query는 말 그대로 JPA쿼리문을 작성가능합니다. DTO로 생성도 가능합니다.

//to Entity
@Query("select m.username from MemberSpring m")
List<String> findUsernameList();
//to dto
@Query("select new study.datajpa.dto.MemberSpringDto(m.id, m.username, t.name) " +
 "from MemberSpring m join m.team t")
List<MemberSpringDto> findMemberDto();

@Param 바인딩

이미 위 예제들에서 사용해서 보이시겠지만, 파라미터를 바인딩이 가능합니다.

 //Param으로 직접 선언하여 사용가능합니다.
 @Query("select m from MemberSpring m where m.username = :name")
 MemberSpring findMembers(@Param("name") String username);
 // WHERE IN 절을 사용하기 위한 Collection 타입도 가능합니다.
 @Query("select m from MemberSpring m where m.username in :names")
 List<MemberSpring> findByNames(@Param("names") List<String> names);

Paging 작업

Sping DATA는 다음과 같이 페이징작업을 편리매소드로 제공합니다.

  • 페이징과 정렬 파라미터
    - org.springframework.data.domain.Sort : 정렬 기능
    - org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
  • 특별한 반환 타입
    - org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
    - org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적
    으로 limit + 1조회)
    - List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

Page<MemberSpring> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<MemberSpring> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<MemberSpring> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<MemberSpring> findByUsername(String name, Sort sort);
List<MemberSpring> findTop3By(); // 쿼리 매소드를 사용하여 다음과 같은 기능을 이용가능
List<MemberSpring> findTop3By(); // 쿼리 매소드를 사용하여 다음과 같은 기능을 이용가능

설명에서 나타나듯 count() 쿼리를 강제적으로 선언하는 것, 쿼리문이 비정상적으로 join되어 있는 경우 속도의 저하를 일으킬 수 있습니다. 다음과 같이 선언하여 countQuery를 최적화한 쿼리로 돌아가게 만들 수 있습니다.

@Query(value = "select m from MemberSpring m", countQuery = "select count(m.username) from MemberSpring m")

pageable 은 다음과 같이 객체를 생성하시면 됩니다.

//PageRequest.of(page: 페이지 수 0부터 시작입니다.,size: 출력갯수,sort: 정렬)
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<MemberSpring> page = memberRepository.findByAge(10, pageRequest);

page 값을 불러와 다양하게 사용가능합니다.

 List<MemberSpring> content = page.getContent(); //조회된 데이터
 assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
 assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
 assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
 assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
 assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
 assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?

bulk update

JPA는 Entity단위로 저장 수정 삭제가 발생하기 때문에 대량에 데이터를 처리하기에는 불편한 기능입니다. 그래도 bulk 단위로 사용가능하게 만들어져있는 몇가지 기능이 있습니다.

@Modifying
@Query("update MemberSpring m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

요렇게 선언하면 데이터를 한번에 업데이트하고 commit이 될겁니다. 단, 이 경우 영속성 컨테이너를 컨트롤 하지 않으므로 해당데이터를 조회할 경우 영속성 컨테이너에 기존에 업데이트 전 데이터를 조회를 해옵니다. EntityManager를 clear() 해줘야하죠...@Modifying에는 이러한 옵션을 제공합니다.

@Modifying(clearAutomatically = true) 

@EntityGraph

fetch join을 어노테이션화한 기능입니다.

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<MemberSpring> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from MemberSpring m")
List<MemberSpring> findMemberSpringEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<MemberSpring> findByUsername(String username)

@NamedQuery 처럼 Entity에 사용가능합니다. @NamedEntityGraph

//Entity에 선언
@NamedEntityGraph(name = "MemberSpring.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class MemberSpring {}

//Repository에 선언
@EntityGraph("MemberSpring.all")
@Query("select m from MemberSpring m")
List<MemberSpring> findMemberSpringEntityGraph();

JPA Hint & Lock

쿼리에 힌트를 추가하여 다양한 기능을 제공받을 수 있습니다. 여기서는 readOnly를 사용합니다. SQL에 HINT하고 다릅니다.
페이징을 사용하는 JPA 쿼리의 경우 forCounting을 적용합니다.

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
MemberSpring findReadOnlyByUsername(String username);
// 페이지를 사용하는 쿼리에도 추가 가능합니다.
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<MemberSpring> findByUsername(String name, Pageable pageable);

readOnly, @Transation vs @QueryHints

@Transaction(readOnly=true)는 트랜잭션 커밋 시점에 flush를 하지 않기 때문에 이로 인한
변경감지 (dirty checking) 비용이 들지 않습니다. 따라서 cpu가 절약됩니다.
@QueryHint의 readOnly는 스냅샷을 만들지 않기 때문에, 메모리가 절약됩니다.
즉, 둘 다 선언하셔도 각각의 이점을 챙기실 수 있습니다.

@Lock의 경우에는 우리가 알고있는 SQL의 LOCK의 기능을 합니다. 동시성 문제를 해결하기 위해 자료를 수정못하게 잠궈놓는 역할을 합니다. 더 많은 예제는 찾아보셔야합니다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<MemberSpring> findByUsername(String name);

Custom Repository 구현

Spring DATA는 우리가 구현한 쿼리 매소드도 상속받아 구현해줍니다.
사용자 매소드를 작성할 클래스를 만들어야합니다.

public interface MemberRepositoryCustom {
 	List<MemberSpring> findMemberCustom();
}
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

   private final EntityManager em;

   @Override
   public List<MemberSpring> findMemberCustom() {
      return em.createQuery("select m from MemberSpring m").getResultList();
   }
}

이때 중요한것은 ...Impl이라고 파일을 만드셔야 Repository에서 인식한다는 점입니다.

XML 설정

<repositories base-package="study.datajpa.repository"
 repository-impl-postfix="Impl" />

JavaConfig 설정

@EnableJpaRepositories(basePackages = "study.datajpa.repository",
 repositoryImplementationPostfix = "Impl")

이제 만들어놓은것을 적용하면 됩니다. 다음과 같이 extends JpaREpository뒤에 MemberRepositoryCustom를 선언하시면 됩니다.

public interface MemberRepository 
				extends JpaRepository<MemberSpring, Long>, MemberRepositoryCustom {
}

그 다음엔 그냥 사용하시면 됩니다.

List<MemberSpring> result = memberRepository.findMemberCustom();

이렇게 만듦으로써 사용자 쿼리와, JPA Spring DATA가 자동으로 만들어주는 쿼리 매소드를 따로 분리하여 관리하는 것이 가능합니다.

@Audting

운영서버를 많이 경험해보신 실무 경험자들이라면 db데이터를 insert시에 마스터 테이블 컬럼에 항상 생성, 수정에 대한 시간 및 사용자의 정보가 들어가있는것을 알 수 있습니다. 해당 컬럼이 운영관리자들에게 주는 이점이 굉장히 크거든요. 데이터를 통계내리기도 좋구요.. @Audting은 이러한 컬럼을 자동으로 생성해줍니다.

JPA 방식
직전에 실행해주는 @Pre...를 이용하여 now() date를 생성하여 직접 관리가 됩니다.

 //첫 데이터 insert시에만 작동하고 update시에는 createDate가 작동하지 않습니다.
 @Column(updatable = false) 
 private LocalDateTime createdDate;
 private LocalDateTime updatedDate;
 @PrePersist
 public void prePersist() {
 LocalDateTime now = LocalDateTime.now();
 createdDate = now;
 updatedDate = now;
 }
 @PreUpdate
 public void preUpdate() {
 updatedDate = LocalDateTime.now();
 }

Spring DATA 방식
우선 boot에 설정해줘야합니다. @EnableJpaAuditing 추가

@EnableJpaAuditing // 선언
@SpringBootApplication
//생략...

등록 수정을 관리할 Entity파일을 @MappedSuperclass로 선언하여 공통 컬럼 소스로 만들어 버리고 @EntityListeners(AuditingEntityListener.class)를 선언해줍니다.

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
   @CreatedDate
   @Column(updatable = false)
   private LocalDateTime createdDate;
   @LastModifiedDate
   private LocalDateTime lastModifiedDate;
   @CreatedBy
   @Column(updatable = false)
   private String createdBy;
   @LastModifiedBy
   private String lastModifiedBy;
}

테스트해보시면 정상적으로 시간이 찍힙니다.

Member findMember = memberRepository.findById(member.getId()).get();

System.out.println("findMember.createdDate = " + findMember.getCreatedDate());
System.out.println("findMember.updatedDate = " + findMember.getUpdatedDate());

createdBy, lastModifiedBy도 마찬가지로 선언이 가능합니다만, 맵핑할 대상을 @Bean으로 등록해줘야합니다.

	@Bean
	public AuditorAware<String> auditorProvider() {
		//uuid에는 원래 아이디가 들어갈 자리다 userId값이 있으면 불러와 setting하면 됩니다.
        //테스트이니 random사용 
		return () -> Optional.of(UUID.randomUUID().toString());
	}

결론

MVC에 자동으로 맵핑해주고 동적쿼리 관리 및 객체파일 생성없이 DTO화 하는 방법 등이 더 있지만 다음에 배울 querydsl이 있어서 여기까지 설명하겠습니다. 필요하면 다시 수정해서 올리면 되죠... 끗

profile
하이

0개의 댓글