BACK/JPA

[JPA] 실무 활용 - 순수 JPA 와 Querydsl

연듀 2024. 3. 15. 21:17

순수 JPA 리포지토리와 Querydsl

 

순수 JPA 리포지토리로 Querydsl을 사용해보자 

@Repository
public class MemberJpaRepository {
    private final EntityManager em; // 순수 JPA 접근
    private final JPAQueryFactory queryFactory; // querydsl 사용 위함

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }
    

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

    public Optional<Member> findById(Long id){
        Member findMember = em.find(Member.class, id);
        return Optional.ofNullable(findMember);
    }

    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();

    }

 

 

@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {

    @Autowired
    EntityManager em;

    @Autowired
    MemberJpaRepository memberJpaRepository;

    @Test
    public void basicTest(){
        Member member = new Member("member1", 10);
        memberJpaRepository.save(member);

        Member findMember = memberJpaRepository.findById(member.getId()).get();
        assertThat(findMember).isEqualTo(member);

        List<Member> result1 = memberJpaRepository.findAll_Querydsl();
        assertThat(result1).containsExactly(member);

        List<Member> result2 = memberJpaRepository.findByUsername_Querydsl("member1");
        assertThat(result2).containsExactly(member);
    }

}

 

참고로 JPAQueryFactory 를 빈으로 등록해서 주입받아 사용해도 된다.

 

@SpringBootApplication
public class QuerydslApplication {

	public static void main(String[] args) {
		SpringApplication.run(QuerydslApplication.class, args);
	}

	@Bean
	JPAQueryFactory jpaQueryFactory(EntityManager em){
		return new JPAQueryFactory(em);
	}
}

 

 public MemberJpaRepository(EntityManager em, JPAQueryFactory queryFactory) {
        this.em = em;
        this.queryFactory = queryFactory;
}
// = @RequiredArgsConstructor

 

이렇게 빈으로 등록하면 같은 객체를 모든 멀티쓰레드에서 사용하는데, 괜찮을까?

→ 괜찮다.

JPAQueryFactory에 대한 동시성 문제는 엔티티 매니저에 의존한다.

엔티티매니저가 스프링을 엮어서 쓰면 동시성 문제와 관계 없이 트랜잭션 단위로 따로따로 분리돼서 동작하게 된다.

스프링에서는 진짜 영속성 컨텍스트 매니저가 아니라 가짜 프록싱 객체를 주입한다. 트랜젝션 단위로 다 다른데 바인딩 되도록 라우팅만 해준다.

 

동적 쿼리와 성능 최적화 조회 - Builder 사용

@Data
public class MemberSearchCondition {
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}

 

MemberJpaRepository

    public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition){

        BooleanBuilder builder = new BooleanBuilder();
        if (hasText(condition.getUsername())) {
            builder.and(member.username.eq(condition.getUsername()));
        }
        if (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();
    }
@Test
    public void searchTest(){
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        MemberSearchCondition condition = new MemberSearchCondition();
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

        // 만약에 조건이 없으면 모든 데이터를 가져온다. 웬만해서는 기본 조건이나 limit 가 있는게 좋다. 가급적이면 페이징 쿼리가 같이 나가야 한다.

        List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);
        assertThat(result).extracting("username").containsExactly("member4");

    }

 

 

동적 쿼리와 성능 최적화 조회 - Where 절 파라미터 사용

    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();
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null; 
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null; 
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }

 

만약 DTO가 아닌 엔티티를 조회하는 것처럼 select projection 이 달라져도 조건(메소드)들을 재사용 할수있다는 장점이 있다.

아래와 같이 조건을 조립할 수도 있다.

 

    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()))
                ageBetween(condition.getAgeLoe(), condition.getAgeGoe()))
                .fetch();
    }

    private BooleanExpression ageBetween(int ageLoe, int ageGoe){
       return  ageGoe(ageLoe).and(ageGoe(ageGoe));
    }

 

조회 API 컨트롤러 개발

 

로컬에서 메인 소스코드를 Tomcat으로 돌릴 때랑 테스트 케이스를 돌릴 때랑 프로파일을 분리하도록 하자.

 

프로파일 설정

applicatinon.yml

spring:
  profiles:
    active: local # 추가

 

테스트는 src/test/resources

밑에 application.yml 을 복사하고 active 를 test 로 수정한다.

그럼 test 를 실행 시에는 active: test 가 되어 initMember가 실행이 안되고

애플리케이션 실행 시에만 실행 된다.

controller 폴더에 initMember 파일을 만든다.

 

@Profile("local")
@Component
@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() { // PostConstructor 하는 부분과 트랜잭션 동작하는 부분은 분리를 해줘야 한다.
            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));
            }
        }
    }
}

 

 

MemberController

@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberJpaRepository memberJpaRepository;

    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
        return memberJpaRepository.search(condition);
    }
}

 

데이터들이 필터링되어 condition에 따라 출력되는 것을 확인할 수 있다. 

 

 

 

 

인프런 - 실전! Querydsl

https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84