[3] 조회를 담당하는 Query Service

안상철·2022년 8월 16일
0

Kotlin Spring Boot

목록 보기
4/14
post-thumbnail

이 페이지에서는 Query서비스와 Command서비스에 대해 알아봅시다.

디렉토리 구조 및 설명 포스팅에서 설명했던 CQRS패턴의 C와 Q입니다.

1. Query

Query는 조회만 수행합니다. 한건, 다건, 페이지 조회까지 알아보도록 합시다.

1-1) Service class

@Service
@Transactional(readOnly = true)
class MemberQueryService(   

  private val ...
  
) {

    fun...
}

먼저 서비스 클래스를 작성 해 봅시다. Component Scan으로 해당 서비를 읽어 Bean으로 들여올 수 있도록 @Service 어노테이션을 붙여줍시다.

@Transactional 어노테이션을 이용해 이 클래스의 메서드들이 트랜잭션을 수행할 수 있도록 해 주고 readOnly = true 옵션을 적어줍니다. readOnly = true 옵션은 스프링의 Hibernate Session Flush 모드를(em.setFlushMode(FlushModeType.COMMIT)) 등.. MANUAL로 설정 해 주기 때문에 CUD 작업이 동작하지 않고, 변경감지 등의 업무를 수행하지 않아서 조회 성능이 향상됩니다. 영속성 컨텍스트를 비우는 것은 아닙니다!

또, MultiDataSource + QueryDslSupport 설정 에서 DB를 분리할 때 어느 한 쪽을 읽기 전용으로만 사용한다면 Sub DB에 연결된 모든 서비스에 readOnly = true옵션을 달아주면 됩니다.

1-2) 조회 && DTO

MemberQueryService

@Service
@Transactional(readOnly = true)
class MemberQueryService(

   private val memberRepository: MemberRepository,
   private val commonQueryService: CommonQueryService

) {

   // 단건 조회 예
   fun getByOid(memberOid: Long): MemberOut {
       return MemberOut.fromEntity(memberRepository.getByOid(memberOid))
   }
   
   // 다건 조회 예
   fun getListByMemberRole(MemberRole: String): List<MemberOut> {
       return memberRepository.getListByMemberRole(memberRole).map { MemberOut.fromEntity(it) }
   }
   
   // 페이지 조회 예
   fun getPagedResultByMemberRole(
     memberRole: String?,
     pageable: Pageable,
     sort: Sort
     ): Page<MemberOut> {
       return memberRepository.getPagedResultByMemberRole(memberRole)
       .map { MemberOut.fromEntity(it) }
   }
}

OUT DTO

data class MemberOut(

    val oid: Long?,
    val memberRole: String,
    val memberName: String,
    val memberId: String,

    ) {
    companion object {
        fun fromEntity(e: Member): MemberOut {
            return MemberOut(
                oid = e.oid,
                memberRole = e.memberRole(),
                memberName = e.memberName(),
                memberId = e.memberId(),
            )
        }
    }
}

Repository Custom

interface MemberRepositoryCustom {

    fun getByOid(oid: Long?): Member
    fun getListByMemberRole(memberRole: String): List<Member>
    fun getPagedResultByMemberRole(
      memberRole: String?,
      pageable: Pageable,
      sort: Sort): Page<Member>
}

Repository 구현부

class MemberRepositoryImpl : PrimaryQueryDslSupport(Member::class.java), MemberRepositoryCustom {

    private val qMember = QMember.member

    override fun getByOid(memberOid: Long?): Member {
        return from(qMember)
            .where(
                qMember.deleted.isFalse,
                qMember.oid.eq(memberOid)
            )
            .fetchOne() ?: throw DomainEntityNotFoundException(memberOid!!, Member::class, "요청하신 사용자는 존재하지 않습니다.")
    }
   
    override fun getListByMemberRole(memberRole: String): List<Member> {
        return from(qMember)
            .where(
                qMember.deleted.isFalse,
                qMember.memberRole.eq(memberRole)
            )
            .fetchAll() ?: emptyList()
    }
    
    override fun getPagedResultByMemberRole(memberRole: String): Page<Member>(
        memberRole: String?,
        pageable: Pageable,
        sort: Sort
    ): Page<Member> {
        val result = from(qMember)
            .where(
                qMember.deleted.isFalse,
                eqMemberRole(memberRole)
            )
            .orderBy(
                *sort.map {
                    OrderSpecifier(
                        if (it.isAscending) Order.ASC else Order.DESC,
                        Expressions.path(String::class.java, qMember, it.property),
                    )
                }.toList().toTypedArray()
            )
            .fetchAll()

        val pagedResult = querydsl!!.applyPagination(pageable, result).fetch() ?: emptyList()
        return PageableExecutionUtils.getPage(pagedResult, pageable) { result.fetchCount() }
    }
    
    private fun eqMemberRole(memberRole): BooleanExpression? {
    retrun if(memberRole == null) null
    else qMember.memberRole.eq(memberRole)
    }

}
Member Out DTO

위처럼 Repo에서 데이터를 조회해 View에 전달하는 객체에는 OUT, 컨트롤러에서 들어오는 객체는 IN이라는 접미사를 붙여 생성 해 줍니다.

data class에 전달 해 주고자 하는 멤버 프로퍼티를 적어주고 아래 Companion object 를 정의 해줍니다.

Companion object는 자바의 Static과 유사합니다.
더 자세한 설명은 참고

companion object에는 위처럼 메서드를 작성할 수 도 있고, 인터페이스도 만들 수 있습니다. OUT DTO에는 엔티티를 매개변수로 받아서 조회 한 엔티티 클래스 중 원하는 값을 원하는 구성으로 뽑아주는 메서드를 만들어줍니다.

MemberRepositoryCustom

인터페이스인 커스텀 Repo에 구현 할 메서드과 리턴타입을 적어줍시다.

MemberRepositoryImpl

커스텀 Repo의 메서드를 오버라이드로 구현 해 줍니다.

Repository 구현부의 메서드 일부, 조회

private val qMember = QMember.member

    override fun getByOid(memberOid: Long?): Member {
        return from(qMember)
            .where(
                qMember.deleted.isFalse,
                qMember.oid.eq(memberOid)
            )
            .fetchOne() ?: throw DomainEntityNotFoundException(memberOid!!, Member::class, "요청하신 사용자는 존재하지 않습니다.")
    }

queryDSL을 사용하기 위한 Q클래스를 선언하고 from절부터 작성 해 주면 됩니다. 단건은 fetchOne, 다건은 fetchAll/fetct 를 붙여줍니다.

페이지 조회

override fun getPagedResultByMemberRole(memberRole: String): Page<Member>(
        memberRole: String?,
        pageable: Pageable,
        sort: Sort
    ): Page<Member> {
        val result = from(qMember)
            .where(
                qMember.deleted.isFalse,
                eqMemberRole(memberRole)
            )
            .orderBy(
                *sort.map {
                    OrderSpecifier(
                        if (it.isAscending) Order.ASC else Order.DESC,
                        Expressions.path(String::class.java, qMember, it.property),
                    )
                }.toList().toTypedArray()
            )
            .fetchAll()

        val pagedResult = querydsl!!.applyPagination(pageable, result).fetch() ?: emptyList()
        return PageableExecutionUtils.getPage(pagedResult, pageable) { result.fetchCount() }
    }
    
    private fun eqMemberRole(memberRole): BooleanExpression? {
    retrun if(memberRole == null) null
    else qMember.memberRole.eq(memberRole)
    }

페이지 조회는 3가지 단계를 단계를 거쳐서 값을 조회합니다.

  • 받아온 매개변수의 null 허용 유무

간혹 여러가지의 검색조건을 가지고 있을 때 몇몇가지는 null값을 보내준다고 생각 해 봅시다. 위 예시에서는 memberRole이 null이면 어떤 역할이던 상관없이 조회 하라는 예시입니다.

private fun eqMemberRole(memberRole): BooleanExpression? {
    retrun if(memberRole == null) null
    else qMember.memberRole.eq(memberRole)
    }

Impl클래스 내부에 private으로 메서드를 하나 만들어 줍니다. BooleanExpression은 동적쿼리 처리를 위한 리턴타입으로서 메서드 안에는 조건문을 담아줍니다. BooleanExpression이 null을 리턴하면 이 메서드를 사용하는 조건절은 where 구문에서 아예 빠지게 됩니다. 그래서 위처럼 값이 없으면 memberRole을 파라미터에서 제거, null이 아니라면 일치하는 memerRole이 where문에 들어가게 됩니다.

  • sort의 유무

sort는 컨트롤러 단에서 먼저 선언해준 후 전달 해 줍니다. 따라서 컨트롤러의 파라미터에는 넣지 않습니다. 원한다면 파라미터로 받아와서 바인딩 해 줘도 됩니다.

val sort = Sort.by(Sort.Order.desc("memberNo"))

이렇게 작성 해 주고 sort를 메서드의 매개변수로 담아주면, Impl클래스에서 페이지 처리할 때 memberNo이라는 컬럼 이름을 기준으로 오름/내림차순 정렬해 줍니다.

.orderBy(
                *sort.map {
                    OrderSpecifier(
                        if (it.isAscending) Order.ASC else Order.DESC,
                        Expressions.path(String::class.java, qMember, it.property),
                    )
                }.toList().toTypedArray()
            )
            .fetchAll()

where절이 끝나는 부분부터 orderBy를 적용해 줍니다.

  • List를 Page로 변환

fetchAll까지만 작성하면 Page처리가 되지 않은 일반 리스트를 리턴 하게 됩니다.

val pagedResult = querydsl!!.applyPagination(pageable, result).fetch() ?: emptyList()
        return PageableExecutionUtils.getPage(pagedResult, pageable) { result.fetchCount() }

우리는 조회한 List를 queryDsl의 applyPagination을 적용해 페이지로 변환 후 리턴 해 주면 됩니다.

profile
웹 개발자(FE / BE) anna입니다.

0개의 댓글