<MemberJpaRepository.java>
    public List<Member> findAll(){
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
    public List<Member> findAll_querydsl(){
        return queryFactory.selectFrom(member).fetch();
    }
    
    public List<Member> findByUsername(String username){
        return em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", username)
                .getResultList();
    }
    public List<Member> findByUsername_querydsl(String username){
        return queryFactory.selectFrom(member).where(member.username.eq(username)).fetch();
    }
<MemberTeamDto.java>
@Data
public class MemberTeamDto {
    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;
    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}
<MemberSearchCondition.java>
@Data
public class MemberSearchCondition {
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}
ctrl + shift + enter구문 자동 완성!!
    public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition){
        BooleanBuilder builder = new BooleanBuilder();
        if (StringUtils.hasText(condition.getUsername())) {
            builder.and(member.username.eq(condition.getUsername()));
        }
        if (StringUtils.hasText(condition.getTeamName())) {
            builder.and(team.name.eq(condition.getTeamName()));
        }
        if(condition.getAgeGoe() != null){
            builder.and(member.age.goe(condition.getAgeGoe()));
        }
        if(condition.getAgeLoe() != null){
            builder.and(member.age.loe(condition.getAgeLoe()));
        }
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(builder)
                .fetch();
    }
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                        )
                .fetch();
    }
    // BooleanExpression 이 추후  composition 에 유리
    private BooleanExpression usernameEq(String username) {
        return StringUtils.hasText(username) ? member.username.eq(username) : null;
    }
    private BooleanExpression teamNameEq(String teamName) {
        return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
    }
    private BooleanExpression ageGoe(Integer num) {
        return num != null ? member.age.goe(num) : null;
    }
    private BooleanExpression ageLoe(Integer num) {
        return num != null ? member.age.loe(num) : null;
    }
where 절... 너무 깔끔하잖아!! 좋아좋아 최고!
재사용 및 조립도 가능함!
application.yml 에 profiles -> active: local 추가

test 폴더에 application.yml 추가


다 똑같은데, profiles active 를 test 로 설정!
<InitMember.java> 파일 생성
@Profile("local")
@ComponentScan
@RequiredArgsConstructor
public class InitMember {
    private final InitMemberService initMemberService;
    @PostConstruct
    public void init() {
        initMemberService.init();
    }
    
    @Component
    static class InitMemberService {
        @PersistenceContext
        private EntityManager em;
        @Transactional
        public void init(){
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            em.persist(teamA);
            em.persist(teamB);
            for (int i = 0; i < 100; i++) {
                Team selectedTeam = i % 2 == 0 ? teamA : teamB;
                em.persist(new Member("member" + i, i, selectedTeam));
            }
        }
    }
}
@Profile("local")은 profile local 에서만 실행하겠다는 어노테이션
@ComponentScan은 spring 이 돌아갈떄, component scan 을 하라는 뜻.
@PostConstruct는 시작할때 실행을 하겠다는 뜻
@Component는 spring bean 에 component 로 지정
- 굳이
 InitMemberService를 만들어서 init 해주는 이유는 JPA 기능인@Transactional은@PostConstruct랑 함께 지정할 수가 없기 때문!!
<MemberController.java>
    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition){
        return memberJpaRepository.search(condition);
    }
호출하는 법

기존 <MemberJpaRepository.java>

Spring 데이터 JPA 를 활용한 <MemberRepository.java>

Query dsl 은 사용자 정의 리포지토리를 통해 구현한다.
스프링 데이터 JPA 에서 했던 방식.
<MemberRepositoryCumstom.java> 파일 생성
public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}
<MemberRepositoryImpl.java> 파일 생성
public class MemberRepositoryImpl implements MemberRepositoryCustom{
    private final JPAQueryFactory queryFactory;
    public MemberRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }
    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch();
    }
    ...
    // usernameEq, teamNameEq, ageGoe, ageLoe 함수
}
<MemberRepository.java> 파일
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username);
}

MemberRepositoryCustom이라는 interface를 만들어,MemberRepositoryImpl로 구현
- 이때 구현체는
 JpaRepository를 상속받는 리포지토리 뒤에impl붙은 이름과 같아야함
(MemberRepository,MemberRepositoryImpl)MemberRepository는JpaRepository와<해당 인터페이스>를 상속받으면 됨.
조회 쿼리가 너무 복잡한 경우.
MemberRepository에 합치는 것이 아니라,
새로운MemberQueryRepository를 만드는 것도 좋음.
특화된 기능은 따로 만드는 것도 좋음!! 합치는 것이 무조건 정답이 아님.
- 비즈니스, 설계에 따라 유동적으로 도입할 줄 아는 유연성을 가질 것!!
 
Page, Pageable 을 활용해보기!public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    /*추가*/ Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    /*추가*/ Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

기존 쿼리문에
offset과limit를 추가 해주는 방식으로 pageable 구현이 가능하다!!
PageImpl구현체를 사용하여 Page 타입으로 반환 가능!
테스트 코드
    @Test
    public void searchPageSimpleTest() throws Exception{
        // given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);
        Member m1 = new Member("m1", 10, teamA);
        Member m2 = new Member("m2", 20, teamA);
        Member m3 = new Member("m3", 15, teamB);
        Member m4 = new Member("m4", 25, teamB);
        Member m5 = new Member("m5", 30);
        Member m6 = new Member("m6", 40);
        em.persist(m1);
        em.persist(m2);
        em.persist(m3);
        em.persist(m4);
        em.persist(m5);
        em.persist(m6);
        em.flush();
        em.clear();
        // when
        MemberSearchCondition condition = new MemberSearchCondition();
        PageRequest pageRequest = PageRequest.of(0, 3);
        Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
        // then
        assertThat(result.getSize()).isEqualTo(3);
        assertThat(result.getContent()).extracting("username").containsExactly("m1", "m2", "m3");
    }
이 방법의 경우, getTotal() 을 통해 count 를 불러오는데, 이때 count query에 특정 쿼리를 지정해주면 좋다.

result 를 그냥 fetch 를 활용하여 그대로 받아오고,
fetchCount 를 활용해서, 최적화된 count 함수를 실행함.
핵심은 count 를 직접 지정해주어서, count와 상관 없는 쿼리가 포함되는 것을 막을 수 있음 (성능 최적화)
count 가 0일때는 search 쿼리를 날리지 않는 다던지 하는
데이터가 얼마 없을때는 그냥...fetchResult써라...
        JPAQuery<Member> countQuery = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );
        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
스프링데이터 JPA 에서 PageableExecutionUtils 로 그 기능을 제공해준다.
세번째 input 으로는 page count 가 필요할때 실행할 함수를 넣어주면 된다!
if 문 넣어서 직접 해도 되긴함!!

이런식으로 controller 에서 불러와 사용하면 됨!

여러개 가 있기는 하지만, 복잡한 실무에서 사용하기는 좀 애매함... 참고만!!
QuerydslPredicateExecutor
1 - query dsl 코드를 인자로 넘길 수 있음
2 - left join 지원 안해줌.
3 - 서비스, 컨트롤러 계층에 query 문이 들어감
Querydsl Web 지원
1 - @QuerydslPredicate 를 활용해서 파라미터를 편하게 받을 수 있음
2 - 조건문이 한정적임 / custom 하려면 너무 복잡해짐
3 - 마찬가지로 상위 계층에 query 관련 function 이 포함됨.
QuerydslRepositorySupport
1 - 추상클래스임 (특정 리포지토리에 상속시키면 됨)
2 - queryFactory 없이, 구현된 특정 메소드들을 활용하여 쉽게 메소드 짤 수 있음
김영한 강사님의.
QuerydslRepositorySupport한계 극복 클래스 직접 제작!!
@Repository
public abstract class Querydsl4RepositorySupport {
    private final Class domainClass;
    private Querydsl querydsl;
    private EntityManager entityManager;
    private JPAQueryFactory queryFactory;
    public Querydsl4RepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        this.domainClass = domainClass;
    }
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        JpaEntityInformation entityInformation =
                JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, new
                PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }
    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
        Assert.notNull(queryFactory, "QueryFactory must not be null!");
    }
    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }
    protected Querydsl getQuerydsl() {
        return querydsl;
    }
    protected EntityManager getEntityManager() {
        return entityManager;
    }
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery) {
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaQuery).fetch();
        return PageableExecutionUtils.getPage(content, pageable,
                jpaQuery::fetchCount);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,
            JPAQuery> countQuery) {
        JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaContentQuery).fetch();
        JPAQuery countResult = countQuery.apply(getQueryFactory());
        return PageableExecutionUtils.getPage(content, pageable,
                countResult::fetchCount);
    }
}
이런 식으로 기존에 있던게 불편하다면, 중간에 함수들을 약간 변경해서 내 맛대로 사용해도 되는 거였음!!