(JPA+Boot)레포지토리 구현

JPA 레포지토리 계층에서 인터페이스를 활용한 확장성 있는 데이터 접근 구조를 설계하고, Spring Data JPA와 일반 JPA를 효과적으로 조합하는 방법을 다루는 가이드입니다.



(레포지토리) Repository + 인터페이스

레포지토리는 DB에 데이터 처리하는 역할

확장성을 위해서 “레포지토리,서비스 단에 인터페이스를 활용”할 수 있다.

레포지토리는 Spring Data JPA + JPA 함께 사용 중! -> 게시물 “스프링 DB 관련” 챕터 필독!

  • Spring Data JPA는 JpaReository 인터페이스 로 볼 수 있고,
  • 상속 시 서비스 단에서 바로 사용이 가능하다.
    -> 자동으로 구현체를 만들어 주기 때문
  • Sring Data JPA 기본 제공 CRUD표

단, 아래 예시 코드는 어댑터추가ver-type2이고, 본인은 보통 단순ver을 사용!

예시 코드 보기 - 레포지토리 어댑터추가ver


MemberRepository 인터페이스 + MemberRepositoryCustom 인터페이스 + MemberRepositoryCustomImpl 클래스 의 조합

/**
 * MemberRepository 인터페이스 -> 서비스 계층에서 사용하게 될 본체
 * 여기서 2개의 인터페이스를 상속 받는다.
 * - JpaRepository<Member, Long> 이 바로 Spring Data JPA 이다.
 * - MemberRepositoryCustom 이 바로 일반 JPA 이다. 
 */
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
  //CRUD 자동 제공
}
/**
 * MemberRepositoryCustom 인터페이스 -> 일반 JPA로 커스텀
 */
public interface MemberRepositoryCustom {
  Member findOne(Long id);
  Member findByUid(String uid);
  List<FindMemberResponseDto> findAllWithPage(int pageId);
}
/**
 * MemberRepositoryCustomImpl 클래스 -> 일반 JPA로 커스텀 구현체
 * - save() 주석 -> JpaRepository 꺼 사용하려고
 * - @Override -> 인터페이스 구현 때 당연히 필수 키워드
 */
@Repository
@RequiredArgsConstructor // 생성자 주입 + 엔티티매니저 주입 제공
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
  private final EntityManager em;
  /**
   * save, findOne, findByUid, findAllWithPage
   */
//  public void save(Member member) {
//    if (member.getId() == null) {
//      em.persist(member); // db 저장
//    }
//  }
  @Override
  public Member findOne(Long id) {
    return em.find(Member.class, id);
  }
  @Override
  public Member findByUid(String uid) {
    List<Member> findMembers = em.createQuery("select m from Member m where m.uid = :uid",
            Member.class)
        .setParameter("uid", uid)
        .getResultList(); // List로 반환 받아야 null처리가 쉬움
    return findMembers.isEmpty() ? null : findMembers.get(0);
  }
  @Override
  public List<FindMemberResponseDto> findAllWithPage(int pageId) {
    int offset = (pageId - 1) * 10;
    int limit = 10;
    List<Object[]> objects = em.createNativeQuery("select m.member_id, m.nickname, e.level " +
            "from (select * from member order by member_id desc limit " + offset + "," + limit + ") m "
            +
            "inner join character c on m.character_id=c.character_id " +
            "inner join exp e on c.exp_id=e.exp_id;")
        .getResultList();
    return objects.stream()
        .map(o -> new FindMemberResponseDto((Long) o[0], (String) o[1], (Long) o[2]))
        .collect(Collectors.toList());
  }
}


코드 작성 TIP:

  • @Repository: 레포지토리 빈 자동 등록
  • 레포에서 em.createQuery()할때 .getResultList(); 로 반환받는게 null처리 용이!! (보통 하나를 받더라도 이걸로 하는 중!)

  • Spring Data JPA를 사용해보면 Optional<> 로도 반환하므로 orElse(null) 또는 orElseThrow(new ... ) 등 알기!
    • 단, Member 이런식으로 반환타입 명시하면 Optional<>로 반환 안함!
  • DTO: RequestDto, ResponseDto는 JsonIgnore문제에도 자유롭다!(DTO Lazy강제초기화 필수)
    • 주로 컨트롤러 단에서 내부 엔티티 보호 목적으로 사용! -> “컨트롤러 파트 보자”
    • 따라서 “미리 요청,응답 양식이 있는게 아니라면” 컨트롤러 단 개발할 때 Dto 고려
    • 테스트코드 작성엔 DTO 생길수록 코드수정이 늘어나긴 함
  • @Transactional 작성에 주의하자.
    • 일반 JPA를 사용할 때는 서비스 계층의 @Transactional 범위 내에서 동작하지만, Spring Data JPA는 각 메서드가 독립적인 트랜잭션으로 실행될 수 있어 차이가 발생!
    • 서비스 계층(상위 계층)에 @Transactional이 또 있으면 해당(상위) 범위로 동작
    • EntityManager는 트랜잭션 범위의 영속성 컨텍스트를 사용 하므로 당연히 @Transactional 필요
    • 참고 TDD 코드 플젝: entertainment 플젝
      • 자세한 JPQL 관련 개념은 아래 “JPQL파트”에서 참고



댓글남기기