no image
[인프런 알고리즘] Chapter 6, 5번 문제(중복 확인)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.HashSet;import java.util.StringTokenizer;public class sec06_05 { public static char solution(int[] arr) { HashSet set = new HashSet(); for (int i : arr) set.add(i); return (arr.length == set.size())..
2024.08.18
no image
[Query DSL] 기본 문법
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.EntityManager 로 JPAQueryFactory 생성Querydsl은 JPQL 빌더JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리JPAQueryFactory를 필드로 제공하면 동시성 문제는 JPAQueryFactory를 생성 할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다. 테스트 기본 코드@Entity@Get..
2024.08.18
no image
[인프런 알고리즘] Chapter 06, 4번 문제(Least Recently Used)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec06_04 { public static int[] solution(int S, int[] arr) { int[] cache = new int[S]; for (int i : arr) { int pos = -1; for(int j = 0; j 0; --j) c..
2024.08.17
no image
[Query DSL] Query DSL 초기 세팅
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.필자의 스프링 부트 버전은 3.3.2이다. 의존성 추가build.gradle의 dependencies에 아래의 내용을 추가해준다.implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"이후 gradle을 새로고침 해준다. 환경 설정 검증테스트용 엔티티를 하나 추가해준다./src/main/java/study/querydsl/controller/entity/Hell..
2024.08.17
no image
[인프런 알고리즘] Chapter 6, 3번 문제(삽입 정렬)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec06_03 { public static int[] solution(int[] arr) { for(int i = 1; i = 0; --j) { if(arr[j] > targetValue) arr[j + 1] = arr[j]; else break; ..
2024.08.16
no image
[Spring Data JPA] Projections 과 Native Query
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.ProjectionsProjections는 Spring Data JPA에서 특정 엔티티의 일부 필드만을 선택적으로 조회하기 위해 사용하는 기능이다. Projections를 통해 전체 엔티티를 조회하지 않고 필요한 필드만을 선택적으로 가져올 수 있으며, 이를 통해 성능을 최적화하고 데이터 전송량을 줄일 수 있다. 인터페이스 기반 프로젝션 (Interface-based Projection)프로젝션을 위해 인터페이스를 정의하고, 필요한 필드만 메서드로 선언한다.JPA는 이 인터페이스를 구현하는 프록시 객체를 생성하여, 쿼리 결과를 이 인터페이스의 구현체로 반환한다.public interface UsernameOnly ..
2024.08.16
no image
[인프런 알고리즘] Chpater 6, 2번 문제 (버블 정렬)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec06_02 { public static int[] solution(int[] arr) { for(int i = 0; i arr[j + 1]) { int tmp = arr[j + 1]; arr[j + 1] = arr[j]; ..
2024.08.15
no image
[인프런 알고리즘] Chpater 6, 1번 문제(선택 정렬)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec06_01 { public static int[] solution(int[] arr) { for(int i = 0; i  설명2023.08.21 - [자료구조 & 알고리즘/알고리즘] - [알고리즘] 선택 정렬과 빅 오(Big O) [알고리즘] 선택 정렬과 빅 오(Big O)이 글은 누구나 자료 구조와 알고리즘(저자 : 제..
2024.08.15

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.StringTokenizer;

public class sec06_05 {
    public static char solution(int[] arr) {
        HashSet<Integer> set = new HashSet<>();
        for (int i : arr) set.add(i);
        return (arr.length == set.size()) ? 'U' : 'D';
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for (int i = 0; i < arr.length; ++i) arr[i] = Integer.parseInt(st.nextToken());
        System.out.println(solution(arr));
    }
}

 

설명

  • 배열 arr에 있는 모든 값을 HashSet에 추가한다.
  • 만약 배열 arr의 길이와 HashSet의 크기가 같으면 중복이 없다는 뜻이므로 ‘U’를 반환한다.
  • 배열의 길이와 HashSet의 크기가 다르면 중복이 있다는 뜻이므로 ‘D’를 반환한다.
  • HashSet에 값을 추가하는 연산은 평균적으로 O(1)이므로 배열의 크기가 N일 때 전체 시간 복잡도는 O(N)이다. 이는 매우 효율적이다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


  • EntityManager 로 JPAQueryFactory 생성
  • Querydsl은 JPQL 빌더
  • JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)
  • JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
JPAQueryFactory를 필드로 제공하면 동시성 문제는 JPAQueryFactory를 생성 할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.

 

테스트 기본 코드

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);

        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);
    }
    //...
}

 

 

기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법

QMember qMember = new QMember("m");//별칭 직접 지정 -> Member 테이블의 별칭이 m
QMember qMember = QMember.member; //기본 인스턴스 사용 -> Member 테이블의 별칭이 member

같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용 권장

 

기본 인스턴스를 static import와 함께 사용

 import static study.querydsl.entity.QMember.*;

@Test
 public void startQuerydsl3() {
    //member1을 찾아라.
    Member findMember = queryFactory
                 .select(member)
                 .from(member)
                 .where(member.username.eq("member1"))
                 .fetchOne();
                 
         assertThat(findMember.getUsername()).isEqualTo("member1");
 }

 

 

검색 조건 쿼리

기본 검색 쿼리

@Test
public void search() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1").and(member.age.eq(10)))
            .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 검색 조건은 .and(), .or() 를 메서드 체인으로 연결할 수 있다.
  • 또한 select, from을 selectFrom으로 합칠 수도 있다.

 

JPQL이 제공하는 모든 검색 조건 제공

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30


member.username.like("member%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색

 

AND 조건을 파라미터로 처리

@Test
public void searchAndParam() {
    List<Member> result1 = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"), member.age.eq(10))
            .fetch();

    assertThat(result1.size()).isEqualTo(1);
}
  • where() 에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨
  • 이 경우 null 값은 무시 -> 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있음

 

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    -결과가 없으면 : null
    -결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
        .selectFrom(member)
		.fetch();

//단 건
Member findMember1 = queryFactory
        .selectFrom(member)
        .fetchOne();

//처음 한 건 조회
Member findMember2 = queryFactory
        .selectFrom(member)
        .fetchFirst();

//페이징에서 사용
QueryResults<Member> results = queryFactory
        .selectFrom(member)
        .fetchResults();
        
//count 쿼리로 변경
long count = queryFactory
         .selectFrom(member)
         .fetchCount();

 

정렬

  • desc() , asc() : 내림차순, 오름차순 정렬
  • nullsLast() , nullsFirst() : null 데이터 순서 부여(last는 마지막에, first는 맨 앞으로 정렬)
/**
 * 회원 정렬 순서
 * 1. 회원 나이 내림차순(desc)
 * 2. 회원 이름 올림차순(asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last) */
@Test
public void sort() {
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(), member.username.asc().nullsLast())
            .fetch();

    Member member5 = result.get(0);
    Member member6 = result.get(1);
    Member memberNull = result.get(2);

    assertThat(member5.getUsername()).isEqualTo("member5");
    assertThat(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}

 

페이징

조회 건수 제한

@Test
public void paging1() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1) //0부터 시작(zero index)
            .limit(2) //최대 2건 조회
            .fetch();

    assertThat(result.size()).isEqualTo(2);
}

 

전체 조회수

@Test
public void paging2() {
    QueryResults<Member> queryResults = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2)
            .fetchResults();

    assertThat(queryResults.getTotal()).isEqualTo(4);
    assertThat(queryResults.getLimit()).isEqualTo(2);
    assertThat(queryResults.getOffset()).isEqualTo(1);
    assertThat(queryResults.getResults().size()).isEqualTo(2);
}

이 쿼리는 Querydsl을 사용하여 특정 조건에 맞는 Member 엔티티 데이터를 페이징 처리하여 조회하는 것이다.

여기서 fetchResults() 메서드는 페이징된 데이터뿐만 아니라 전체 데이터셋에 대한 정보도 함께 반환한다.

 

구체적으로, 이 메서드는 두 가지 중요한 정보를 제공한다

  • 페이징된 결과: 쿼리의 offset과 limit에 따라 제한된 개수의 데이터를 가져온다.
  • 전체 조회수: 이 쿼리로 제한된 데이터뿐만 아니라 전체 데이터의 개수를 파악할 수 있다.

 

ueryResults.getTotal()

  • 전체 조회수를 반환한다.
  • 이 값은 쿼리에서 페이징 처리가 이루어지지 않은 경우의 총 레코드 수이다. 예를 들어, 데이터베이스에 4개의 Member가 있다면, 전체 조회수는 4가 된다.

 

fetchResults()의 동작 방식

  • Querydsl은 fetchResults()를 호출할 때 두 번의 쿼리를 실행한다. 첫 번째 쿼리는 COUNT 쿼리로, 전체 레코드 수를 계산한다. 두 번째 쿼리는 실제 데이터를 가져오는 쿼리로, offset과 limit에 따라 페이징된 결과를 반환한다.
  • 이 방식으로 전체 데이터셋의 크기와, 페이징된 데이터셋을 동시에 얻을 수 있다.

 

fetchResults()를 통해 쿼리를 실행하고 결과를 가져온다.

fetchResults()는 쿼리 결과와 함께 총 레코드 수, 오프셋, 제한 수 등을 포함한 QueryResults 객체를 반환한다.

 

count 쿼리가 실행되니 성능상 주의!


실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리 는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.

 

 

집합

집합 함수

/**
 * JPQL
 * select
 * COUNT(m),
 *    SUM(m.age),
 *    AVG(m.age),
 *    MAX(m.age),
 *    MIN(m.age)
 * from Member m
 */
@Test
public void aggregation() throws Exception {
    List<Tuple> result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
    Tuple tuple = result.get(0);

    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}

 

GroupBy 사용

/**
 * 팀의 이름과 각 팀의 평균 연령을 구해라. */
@Test
public void group() throws Exception {
    QMember member = QMember.member;
    QTeam team = QTeam.team;

    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();

    Tuple teamA = result.get(0);
    Tuple teamB = result.get(1);

    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);
    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
  1. Member와 Team을 조인하여 각 팀의 이름과 팀에 속한 모든 회원들의 평균 나이를 선택한다.
  2. groupBy(team.name)을 사용하여 팀 이름별로 데이터를 그룹화하고, 그룹화된 데이터에 대해 member.age.avg()를 계산하여 각 팀의 평균 연령을 구한다.
  3. 쿼리 결과는 각 팀별 이름과 그 팀의 평균 연령으로 이루어진 Tuple 리스트로 반환된다.

 

groupBy(), having() 예시

//...
     .groupBy(item.price)
     .having(item.price.gt(1000))
//...

 

 

Join

기본 조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

join(조인 대상, 별칭으로 사용할 Q타입)

 

/**
 * 팀 A에 소속된 모든 회원 */
@Test
public void join() throws Exception {
    QMember member = QMember.member;
    QTeam team = QTeam.team;

    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    for (Member m : result) {
        System.out.println("m = " + m + " " + m.getTeam());
    }
}
  • join() , innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin()` : rigth 외부 조인(rigth outer join)
  • JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공

 

세타 조인

연관관계가 없는 필드로 조인

/**
 * 세타 조인(연관관계가 없는 필드로 조인) * 회원의 이름이 팀 이름과 같은 회원 조회 */
@Test
public void theta_join() throws Exception {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    List<Member> result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부 조인 불가능 다음에 설명할 조인 on을 사용하면 외부 조인 가능

 

ON 절

조인 대상 필터링

예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

/**
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
 t.name='teamA'
 */
@Test
public void join_on_filtering() throws Exception {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team)
            .on(team.name.eq("teamA"))
            .fetch();
    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}

 

on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절 에서 필터링 하는 것과 기능이 동일하다.

따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.

 

연관관계 없는 엔티티 외부 조인

예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

/**
 * 2. 연관관계 없는 엔티티 외부 조인
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name */
@Test
public void join_on_no_relation() throws Exception {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team).on(member.username.eq(team.name))
            .fetch();
    for (Tuple tuple : result) {
        System.out.println("t=" + tuple);
    }
}

문법을 잘 봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.

  • 일반조인: leftJoin(member.team, team)
  • on조인: from(member).leftJoin(team).on(xxx)

 

FETCH 조인

페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.

 

FETCH 조인 미적용

지연로딩으로 Member, Team SQL 쿼리 각각 실행

@PersistenceUnit
EntityManagerFactory emf;

@Test
public void fetchJoinNo() throws Exception {
    em.flush();
    em.clear();
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isFalse();
}

 

FETCH 조인 적용

즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회

@Test
public void fetchJoinUse() throws Exception {
    em.flush();
    em.clear();
    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq("member1"))
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 적용").isTrue();
}

join(), leftJoin()` 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.

 

서브 쿼리

com.querydsl.jpa.JPAExpressions 사용

 

WHERE 절에 서브쿼리

서브 쿼리 eq 사용

/**
 * 나이가 가장 많은 회원 조회 */
@Test
public void subQuery() throws Exception {
    QMember memberSub = new QMember("memberSub");
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    JPAExpressions
                            .select(memberSub.age.max())
                            .from(memberSub)
            )) .fetch();

    assertThat(result).extracting("age")
            .containsExactly(40);
}

 

서브 쿼리 goe 사용

/**
 * 나이가 평균 나이 이상인 회원 */
@Test
public void subQueryGoe() throws Exception {
    QMember memberSub = new QMember("memberSub");
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
            )) .fetch();

    assertThat(result).extracting("age")
            .containsExactly(30,40);
}

 

SELECT 절에 서브쿼리

/**
 * 서브쿼리 여러 건 처리, in 사용 */
@Test
public void subQueryIn() throws Exception {
    QMember memberSub = new QMember("memberSub");
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))
            )) .fetch();

    assertThat(result).extracting("age")
            .containsExactly(20, 30, 40);
}

 

static import 활용

import static com.querydsl.jpa.JPAExpressions.select;

List<Member> result = queryFactory
     .selectFrom(member)
     .where(member.age.eq(
             select(memberSub.age.max())
    		.from(memberSub
    )) .fetch();

 

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
당연히 Querydsl도 지원하지 않는다.
하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용 하면 select 절의 서브쿼리를 지원한다.

 

from 절의 서브쿼리 해결방안

1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.

 

Case 문

select, 조건절(where), order by에서 사용 가능

 

단순한 조건

List<String> result = queryFactory
         .select(member.age
            .when(10).then("열살")
            .when(20).then("스무살")
            .otherwise("기타"))
        .from(member)
        .fetch();

 

복잡한 조건

List<String> result = queryFactory
        .select(new CaseBuilder()
            .when(member.age.between(0, 20)).then("0~20살")
            .when(member.age.between(21, 30)).then("21~30살")
            .otherwise("기타"))
        .from(member)
        .fetch();

 

예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
1. 0~30살이아닌회원을가장먼저출력
2. 0~20살회원출력
3. 21~30살회원출력

 NumberExpression<Integer> rankPath = new CaseBuilder()
         .when(member.age.between(0, 20)).then(2)
         .when(member.age.between(21, 30)).then(1)
         .otherwise(3);
         
 List<Tuple> result = queryFactory
         .select(member.username, member.age, rankPath)
         .from(member)
         .orderBy(rankPath.desc())
         .fetch();
         
 for (Tuple tuple : result) {
     String username = tuple.get(member.username);
     Integer age = tuple.get(member.age);
     Integer rank = tuple.get(rankPath);
     System.out.println("username = " + username + " age = " + age + " rank = " + rank);
}

Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.

 

 

상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용

Tuple result = queryFactory
         .select(member.username, Expressions.constant("A"))
         .from(member)
         .fetchFirst();
위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 상수를 더하는 것 처럼 최적화가 어려 우면 SQL에 constant 값을 넘긴다.

 

 

문자 더하기 concat

String result = queryFactory
         .select(member.username.concat("_").concat(member.age.stringValue()))
         .from(member)
         .where(member.username.eq("member1"))
         .fetchOne();

결과: member1_10

 

member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다.
이 방법은 ENUM을 처리할 때도 자주 사용한다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec06_04 {
    public static int[] solution(int S, int[] arr) {
        int[] cache = new int[S];
        for (int i : arr)
        {
            int pos = -1;
            for(int j = 0; j < cache.length; ++j) if(cache[j] == i) pos = j; //캐시 히트인지 미스인지 탐색
            if(pos == -1) //Cache Miss
            {
                for(int j = cache.length - 1; j > 0; --j) cache[j] = cache[j - 1]; //모든 작업을 한 칸씩 당긴다.
            }
            else //Chche Hit
            {
                for(int j = pos; j > 0; --j) cache[j] = cache[j - 1]; //pos 부터 맨 앞까지 작업을 한 칸씩 당긴다.
            }
            cache[0] = i; //새로운 작업을 맨 앞에 삽입
        }
        return cache;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int S = Integer.parseInt(st.nextToken());
        int N = Integer.parseInt(st.nextToken());
        int[] arr = new int[N];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < arr.length; ++i) arr[i] = Integer.parseInt(st.nextToken());
        for (int i : solution(S, arr)) System.out.print(i + " ");
    }
}

 

설명

  • 캐시 히트/미스 확인
    -arr의 각 작업을 순서대로 탐색하여, 해당 작업이 캐시 내에 있는지(히트) 없는지(미스)를 확인한다.
    -이를 위해 캐시 배열을 탐색하고, 작업이 캐시에 있으면 그 인덱스를 pos에 저장한다. 만약 캐시에 없으면 pos는 -1이 된다.
  • 캐시 미스 처리
    -만약 해당 작업이 캐시에 없으면(미스), 캐시 내 모든 항목을 한 칸씩 뒤로 밀어낸 후, 새로운 작업을 캐시의 맨 앞에 삽입한다.
  • 캐시 히트 처리
    -작업이 이미 캐시에 있을 경우(히트), 그 작업을 캐시의 맨 앞으로 이동시키기 위해 해당 위치에서부터 앞으로 한 칸씩 당겨온다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


필자의 스프링 부트 버전은 3.3.2이다.

 

의존성 추가

build.gradle의 dependencies에 아래의 내용을 추가해준다.

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"

이후 gradle을 새로고침 해준다.

 

환경 설정 검증

테스트용 엔티티를 하나 추가해준다.

/src/main/java/study/querydsl/controller/entity/Hello.java

당연히 경로는 마음대로 설정해도 좋다.

 @Entity
 @Getter @Setter
 public class Hello {
     @Id @GeneratedValue
     private Long id;
 }

 

검증용 Q 타입 생성

Gradle -> Tasks -> build -> clean

 

Gradle -> Tasks -> other -> compileJava

 

 

Q 타입 생성 확인

실제 엔티티 경로 : /src/main/java/study/querydsl/controller/entity/Hello.java

Q타입 생성 경로 : /build/classes/java/main/study/querydsl/controller/entity/QHello.class

 

Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다.

 

 

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec06_03 {
    public static int[] solution(int[] arr) {
        for(int i = 1; i < arr.length; ++i)
        {
            int targetValue = arr[i];
            int j = i - 1;
            for(; j >= 0; --j)
            {
                if(arr[j] > targetValue) arr[j + 1] = arr[j];
                else break;
            }
            arr[j + 1] = targetValue;
        }
        return arr;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for (int i = 0; i < N; ++i) arr[i] = Integer.parseInt(st.nextToken());
        for (int i : solution(arr)) System.out.print(i + " ");
    }
}

 

설명

2023.08.21 - [자료구조 & 알고리즘/알고리즘] - [알고리즘] 삽입 정렬과 빅 오(Big O)

 

[알고리즘] 삽입 정렬과 빅 오(Big O)

이 글은 누구나 자료 구조와 알고리즘(저자 : 제이 웬그로우)의 내용을 개인적으로 정리하는 글임을 알립니다. 삽입 정렬 삽입 정렬의 수행 순서는 아래와 같다. 1. 첫 번째 패스스루에서 임시로

rebugs.tistory.com

 

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


Projections

Projections는 Spring Data JPA에서 특정 엔티티의 일부 필드만을 선택적으로 조회하기 위해 사용하는 기능이다. Projections를 통해 전체 엔티티를 조회하지 않고 필요한 필드만을 선택적으로 가져올 수 있으며, 이를 통해 성능을 최적화하고 데이터 전송량을 줄일 수 있다.

 

인터페이스 기반 프로젝션 (Interface-based Projection)

  • 프로젝션을 위해 인터페이스를 정의하고, 필요한 필드만 메서드로 선언한다.
  • JPA는 이 인터페이스를 구현하는 프록시 객체를 생성하여, 쿼리 결과를 이 인터페이스의 구현체로 반환한다.
public interface UsernameOnly {
	String getUsername();
}

조회할 엔티티의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회(Projection)

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    //...
    List<UsernameOnly> findProjectionsByUsername(String username);
}

메서드 이름은 자유, 반환 타입으로 인지

 

@Test
public void projections() throws Exception {
    //given
    Team teamA = new Team("teamA");
    em.persist(teamA);
    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);
    em.flush();
    em.clear();
    
    //when
    List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
    
    //then
    Assertions.assertThat(result.size()).isEqualTo(1);
 }

실제 SQL에서도 select절에서 username만 조회(Projection)하는 것을 확인할 수 있다.

 

클래스 기반 프로젝션 (Class-based Projection)

  • DTO(Data Transfer Object) 클래스를 정의하고, 생성자를 통해 필요한 필드를 전달받아 프로젝션 결과를 매핑한다.
  • 이 방법은 주로 명시적인 매핑이 필요할 때 사용된다.
public class UsernameOnlyDto {
private final String username;

    public UsernameOnlyDto(String username) {
    	this.username = username;
    }
    
    public String getUsername() {
    	return username;
    }
}
public interface MemberRepository extends JpaRepository<Member, Long> {
    //...
    List<UsernameOnlyDto> findProjectionsByUsername(String username);
}

 

Native Query

가급적 네이티브 쿼리는 사용하지 않는게 좋음, 정말 어쩔 수 없을 때 사용

최근에 나온 궁극의 방법은  스프링 데이터 Projections과 함께 사용하는 것

 

스프링 데이터 JPA 기반 네이티브 쿼리

  • 페이징 지원
  • 반환 타입
    -Object[]
    -Tuple
    -DTO(스프링 데이터 인터페이스 Projections 지원)
  • 제약
    -Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)
    -JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
    -동적 쿼리 불가

 

네이티브 쿼리

public interface MemberRepository extends JpaRepository<Member, Long> {
     
     @Query(value = "select * from member where username = ?", nativeQuery = true)
     Member findByNativeQuery(String username);
     
 }

 

프로젝션 활용

스프링 데이터 JPA 네이티브 쿼리 + 인터페이스 기반 Projections 활용하여 DTO로 반환

@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
             "FROM member m left join team t ON m.team_id = t.team_id",
             countQuery = "SELECT count(*) from member",
             nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);

 

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec06_02 {
    public static int[] solution(int[] arr) {
        for(int i = 0; i < arr.length - 1; ++i)
        {
            for(int j = 0; j < arr.length - 1 - i; ++j)
            {
                if(arr[j] > arr[j + 1])
                {
                    int tmp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = tmp;
                }
            }
        }
        return arr;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for (int i = 0; i < N; ++i) arr[i] = Integer.parseInt(st.nextToken());
        for (int i : solution(arr)) {
            System.out.print( i + " ");
        }
    }
}

 

설명

  • 외부 루프 (i)
    -배열을 여러 번 순회하며 정렬을 반복한다.
    -첫 번째 순회가 끝나면 배열에서 가장 큰 값이 배열의 마지막으로 “버블”처럼 올라간다.
    -이 과정을 배열의 길이 arr.length - 1 만큼 반복하면서 정렬이 완료된다.

  • 내부 루프 (j)
    -배열의 인접한 두 요소를 비교하는 역할을 한다.
    -배열의 j번째 요소와 j+1번째 요소를 비교하여 arr[j]가 arr[j+1]보다 크면 두 값을 교환한다.
    -각 반복(iteration)마다 가장 큰 요소가 배열의 마지막 부분으로 이동하기 때문에, 내부 루프의 범위는 arr.length - 1 - i로 점점 줄어든다. 즉, 이미 정렬된 부분은 다시 확인하지 않는다.

  • 교환 작업
    -내부 루프에서 arr[j] > arr[j + 1] 조건이 성립하면 두 요소의 값을 서로 교환한다. 이를 통해 작은 값이 앞으로, 큰 값이 뒤로 이동하게 된다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec06_01 {
    public static int[] solution(int[] arr) {
        for(int i = 0; i < arr.length - 1; ++i)
        {
            int min = arr[i];
            int minIdx = i;
            for(int j = i + 1; j <arr.length; ++j)
            {
                if(arr[j] < min)
                {
                    min = arr[j];
                    minIdx = j;
                }
            }
            int tmp = arr[i];
            arr[i] = min;
            arr[minIdx] = tmp;
        }
        return arr;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for (int i = 0; i < N; ++i) arr[i] = Integer.parseInt(st.nextToken());
        for(int i : solution(arr)) System.out.print(i + " ");
    }
}

 

설명

2023.08.21 - [자료구조 & 알고리즘/알고리즘] - [알고리즘] 선택 정렬과 빅 오(Big O)

 

[알고리즘] 선택 정렬과 빅 오(Big O)

이 글은 누구나 자료 구조와 알고리즘(저자 : 제이 웬그로우)의 내용을 개인적으로 정리하는 글임을 알립니다. 선택 정렬 선택 정렬은 아래와 같은 단계를 따른다. 1. 배열의 각 셀을 왼쪽부터 오

rebugs.tistory.com