728x90

포스팅이 늦었다. 3주차 미션인 사다리도 끝나게 되었다.

로또에서보다 난이도가 많이 올라간 느낌이었다.

리뷰어분이 빡세게 그리고 꼼꼼하게 해주신 덕분에 나 자체도 굉장히 성장한것 같다❗

아래는 깃허브 PR 목록이다.

사다리 1주차

사다리 2주차

사다리 3주차

사다리 4주차

테스트

전체적으로 테스트코드를 고민하다가 한번 로직에 손을 대면 저절로 도메인 위주로 구현을 하게 되었다.

테스트를 항상 생각하면서 그리고 테스트를 실행함으로 인해 로직을 구현해 나가야 하는것이 조금 부족했던 챕터였다.

그래서 중간에 리뷰를 받다가 너무 로직이 답답해 보였다.

읽고있던 이펙티브 자바를 접목시켜서 조금 더 나은 로직으로 개선했다.

로직개선으로 문제를 겪었었는데, 그 문제가 바로 절차지향으로 개발했기 때문에 문제였다.

모든 로직을 한군데에 구현해놓으니까 분리하기도 쉽지않고 어떻게 돌아가는지 명확하게 알 수도 없는 그런 로직이 완성되어 있었다.

이걸 일급 컬렉션으로 포장해주고, 모든 엔티티를 작게 유지한다 라는 조건을 생각하면서

개발하게 되니까 확실히 알기도 쉬워졌고, 유지보수성이 좋게 되었다.

무엇보다 인터페이스 그리고 람다에 대해 공부를 많이 해야겠다고 생각했다.

프로그램이 뭘 하는지 어떻게 해야하는지 생각하는 Out-In방식이 아니라

In-Out방식으로 최소한의 객체에서부터 출발하는 생각을 지속적으로 해야한다.

접근방식

처음에 내가 이 사다리를 놓고 접근한 방식은 다리와 세로 기둥을 같이 넣어서 구현하려고 했던게 문제였다.

그러니까 사다리 라는 큰 틀만 놓고 일차원적으로 생각한 결과가 이렇게 된 것이다.

여기가 갈아 엎은 부분

그래서 결국 생각해낸 것은 이라는 객체가 결국 이동해서 결과를 내주는 것인데

점부터 시작해서 왼쪽 오른쪽을 판단하게끔 로직을 구현하니까 점점 조금씩 큰 컬렉션으로 나가지면서 그에 대한 테스트도 조금씩 늘릴 수가 있게 되었다.

요구사항

참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)
pobi,honux,crong,jk

실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)
꽝,5000,꽝,3000

최대 사다리 높이는 몇 개인가요?
5

사다리 결과

pobi  honux crong   jk
    |-----|     |-----|
    |     |-----|     |
    |-----|     |     |
    |     |-----|     |
    |-----|     |-----|
꽝    5000  꽝    3000

결과를 보고 싶은 사람은?
pobi

실행 결과
꽝

결과를 보고 싶은 사람은?
all

실행 결과
pobi : 꽝
honux : 3000
crong : 꽝
jk : 5000

조건

자바 8의 스트림과 람다를 적용해 프로그래밍한다.

  • 규칙
    • 모든 엔티티를 작게 유지한다.
    • 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

위 요구사항에 따라 4명의 사람을 위한 5개 높이 사다리를 만들 경우, 프로그램을 실행한 결과는 다음과 같다.

728x90

'Java' 카테고리의 다른 글

JVM  (0) 2022.08.06
Effective Java 4장 요약  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차 피드백  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차  (0) 2022.08.05
728x90

테스트 코드

자동차 경주에 대한 라이브 피드백 시간인데
느낀점이 있어서 포스팅하게 되었다.

테스트 코드 비교할 경우

우리는 항상 getter, setter 메소드를 많이 써왔다.
그래서 나도 습관처럼 객체를 생성하고 비교를 할 경우에 아래와 같이 코드를 작성했었다.

@Test
void create() {
    Position actual = new Position(5);
    assertThat(actual.getPosition).isEqualTo(5);
}

이런식으로 get 메소드를 사용해서 값을 비교를 했는데
이 방식은 잘못되었다기 보다는 get을 사용하지 않고
객체와 객체를 비교하는 방법을 사용하는 것이 오히려 객체지향적 측면에서 좋을 것 같다.

그렇게 Position 클래스에 equals()hashCode() 를 오버라이드 해주고 객체끼리 비교하게끔 만들어준다.

public class Position {
    private Position position = new Position(0);

    public Position(int position) {
        if (position < 0) {
            throw new IllegalArgumentException("음수는 위치 값이 될 수 없음");
        }
        this.position = position;
    }
}
@Test
void create() {
    Position actual = new Position(5);
    assertThat(actual).isEqualTo(new Position(5));
}

이렇게 객체 두개가 같은지를 구현하면 테스트가 성공하게 된다.

이런식으로 어떤 객체를 생성했을때 객체끼리 비교하는 습관을 들이도록 해보자.

문자열과 원자값을 포장해서 쓰는게 객체지향에서 하기 굉장히 좋은 것이니까 지금부터라도 습관 들이자~😄

이제서야 이펙티브 자바 1장이 이해가 되는것 같다. 경험해봐야 이해가 잘되는 이 기분이 좋다.

일급 컬렉션

필드변수를 하나만 두고 사용하는 클래스이다.

일급 콜렉션을 사용하면 계속해서 객체에게 메세지를 보내서 get메소드 대신 객체에게 위임해서 데이터를 조작하게끔 만들어야 한다.

하나씩 객체들을 포장해서 관리하면 테스트 로직을 짜기에 되게 수월하게 작성할 수 있다.

객체에 메시지를 보내라 = 객체가 하게 만들어라 = 객체에게 위임해라

클래스 역할은 작게할수록, 그리고 클래스를 잘게 쪼갠다면? TDD는 쉬워진다

항상 이것들을 생각하면서 코드를 작성하도록 하자 👍텍스트

728x90

'Java' 카테고리의 다른 글

Effective Java 4장 요약  (0) 2022.08.06
TDD Clean Code with Java 12기 3주차  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차  (0) 2022.08.05
[JPA] 객체 지향 쿼리 심화  (0) 2022.08.05
728x90

로또

2주차 미션은 로또 생성기였다.


Step1 - 문자열 덧셈 계산기


Step2 - 로또(자동)


Step3 - 로또(2등)


Step4 - 로또(수동)

프로그래밍 요구사항이 점점 추가되어 조금 더 제한적인 상황에서 조건문을 사용해야 한다.
주차가 늘어가면서 느끼는것이지만, 테스트 주도 개발을 하게 되니까 안하던 방법이라서 손에 익지는 않았다. 그런데 완성되는 테스트를 먼저 구현하다 보니까 오류가 나는 상황에 대해서 더 생각하고 코드를 구현할 수 있게 되는것 같다.

이 과정을 진행하면서 이펙티브 자바도 같이 읽고 있다. 정적 팩토리 메서드는 이제 꼭 쓰게 되는것 같다.😁 꼭 쓰는것은 또 아니라고 생각해야되는데 일단 무분별하게 생성자로 객체를 생성할 수는 없게 만들어 놨다.

클래스

클래스 부분에서 좀 많은 생각을 했었고 이번 과제에서는 if조건문을 추상 클래스와 추상 메소드를 활용해서 조건문을 처리한 로직이 있다.

아직 2주차 미션임에도 불구하고 예전 코드와 좀 많이 달라졌다는게 눈에 보인다.
단순 로직만 구현을 바꾸는 것도 좋겠지만 그 안의 복잡도도 고쳐가면서 코드를 구현해 나가야겠다.

강의를 정말 신청하길 잘했다는 생각이 들고 이 과정을 완주하는것이 목표니까 최선을 다 해보도록 해야겠다.

아래는 2주차 미션의 요구사항이다.

요구사항

기능 요구사항
로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.

로또 1장의 가격은 1000원이다.
구입금액을 입력해 주세요.
14000
14개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[23, 25, 33, 36, 39, 41]
[1, 3, 5, 14, 22, 45]
[5, 9, 38, 41, 43, 44]
[2, 8, 9, 18, 19, 21]
[13, 14, 18, 21, 23, 35]
[17, 21, 29, 37, 42, 45]
[3, 8, 27, 30, 35, 44]

지난 주 당첨 번호를 입력해 주세요.
1, 2, 3, 4, 5, 6

당첨 통계
---------
3개 일치 (5000원)- 1개
4개 일치 (50000원)- 0개
5개 일치 (1500000원)- 0개
6개 일치 (2000000000원)- 0개
총 수익률은 0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임)

힌트

  • 로또 자동 생성은 Collections.shuffle() 메소드 활용한다.
  • Collections.sort() 메소드를 활용해 정렬 가능하다.
  • ArrayList의 contains() 메소드를 활용하면 어떤 값이 존재하는지 유무를 판단할 수 있다.

프로그래밍 요구사항

  • 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
  • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
  • UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
  • indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
  • 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
  • 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
  • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
  • UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
    • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
      else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
728x90

'Java' 카테고리의 다른 글

TDD Clean Code with Java 12기 3주차  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차 피드백  (0) 2022.08.06
[JPA] 객체 지향 쿼리 심화  (0) 2022.08.05
Effective Java 1장  (0) 2022.08.05
728x90

객체 지향 쿼리 심화

한번에 여러 데이터를 수정할 수 있는 벌크 연산이 있다.

벌크 연산

엔티티를 수정하려면 영속성 컨텍스트 변경 감지 기능이나 병합을 사용하고, 삭제하려면 em.remove() 를 사용한다. 데이터를 하나씩 처리하기엔 너무 오래걸려서 여러개를 동시에 처리할 수 있는 벌크연산이 있다.

벌크 연산은 executeUpdate() 를 사용한다. 이 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.

주의사항

벌크 연산은 영속성 컨텍스트를 통하지 않고 바로 DB에 직접 쿼리를 날린다. 그래서 영속성 컨텍스트에 있는 엔티티와 DB에 있는 테이블의 칼럼 값이 다를 수 있는데, 그래서 이 부분을 주의하여야 한다.

  • em.refresh 사용
    • 엔티티를 사용해야 할 경우엔 DB에서 다시 조회한다.
  • 벌크 연산 먼저 실행
    • 벌크연산을 가장 먼저 실행하여 실행한 후에 조회쿼리를 하면 변경된 것으로 조회가 된다.
  • 벌크연산 수행 후 영속성 컨텍스트 초기화
    • 수행 직후 컨텍스트를 초기화하여 엔티티를 제거했다가 벌크연산이 적용된 DB에서 조회하는 방법

영속성 컨텍스트와 JPQL

쿼리 후 영속 상태인것과 아닌 것

select m from Member m //엔티티 조회 (영속성 O)
select o.address from Order o //임베디드 타입 조회 (영속성 X)
select m.id, m.name from Member m // 필드 조회 (영속성 X)

엔티티 전체를 조회해야만이 영속성 컨텍스트가 관리한다❗️

JPQL로 조회한 엔티티와 영속성 컨텍스트

앞선 포스팅에서 JPQL로 DB에서 조회한 엔티티가 영속성 컨텍스트에 있다면 JPQL로 DB에서 조회한 값은 버리고 영속성 컨텍스트에 있던것을 꺼내온다고 했다.

덮어쓰거나 하게 된다면 컨텍스트안에서 수정 중이었던 데이터가 사라질 수 있어 위험하다.
그래서 영속성 컨텍스트는 엔티티의 동일성을 보장하기 때문에 em.find로 조회를 하던, JPQL을 사용하던 영속성 컨텍스트가 같으면 동일한 엔티티를 반환해준다.

JPQL의 특징

  • JPQL은 항상 DB를 조회한다.
  • JPQL로 조회한 엔티티는 영속 상태이다.
  • 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.

그래서 영속성 컨텍스트의 1차 캐시를 되도록이면 관리하여 많이 이용하는 것이 DB에 부하를 적게 주는 것이고 그게 바람직한 사용방법인것 같다는 나의 견해? 🤔

JPQL과 플러시 모드

flush는 영속성 컨텍스트의 변경 내역을 DB에 동기화 해주는 것이다.
그래서 JPA는 flush가 발생했을 때 쓰기지연 SQL 저장소 라고 했던 저장소에 있던 쿼리들을 쭉 만들어 DB에 반영해준다. flush를 호출하려면 em.flush()를 하거나 flush 모드에 따라 커밋 직전이나 쿼리 실행 직전에 자동 호출된다.

쿼리와 플러시 모드

JPQL은 영속성 컨텍스트 데이터를 고려하지 않고 DB에서 조회하기 때문에 사용할때는 반드시 영속성 컨텍스트의 변경사항을 flush해주어야 한다. 그렇지 않으면 데이터가 섞일 수 있다.

@Test
@DisplayName("쿼리와 플러시 모드")
void queryAndFlushTest() {
    em.setFlushMode(FlushModeType.COMMIT);
    Item item = em.find(Item.class, 1L);
    item.setPrice(2000);

    Object item2 = em.createQuery("select i from Item i where i.price = 2000").getSingleResult();
    System.out.println(item2.toString());
}

flush모드를 commit시에만 플러시로 설정해놓으면

이러한 select쿼리문 후에 에러를 발생한다.


맞는 엔티티를 찾을 수 없다고 나오게 된다.

정리

  • JPQL은 SQL을 추상화하여 특정 DB에 의존하지 않는다.
  • QueryDSL은 JPQL을 만드는 빌더 역할만 하므로 JPQL을 잘 알아야 함!
  • QueryDSL을 사용하면 동적 쿼리를 생성하기가 편리하다.
  • QueryDSL은 JPA가 공식 지원하는 것은 아니지만 직관적이고 편리하다.
  • JPA도 네이티브 쿼리를 지원하지만, 종속적인 SQL을 사용하게 되면 특정 DB에만 한정적인게 된다.
    • JPQL을 최대한 활용 해보고 안되면 그 때 네이티브 SQL을 사용하자😊
  • JPQL은 대용량 수정, 삭제를 할 수 있는 벌크 연산을 지원한다.
728x90

'Java' 카테고리의 다른 글

TDD Clean Code with Java 12기 2주차 피드백  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차  (0) 2022.08.05
Effective Java 1장  (0) 2022.08.05
MockMvc  (0) 2022.08.04
728x90

네이티브 SQL

JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL함수를 지원한다.
근데 특정 DB의 방언과 같은 종속적 기능은 지원하지 않는다.

  • 특정 DB만 지원하는 함수, 문법, SQL 쿼리 힌트
  • 인라인 뷰(from절 서브쿼리), UNION, INTERSECT
  • 스토어드 프로시저

종속적인 기능을 지원하는 방법은

  • 특정 DB만 사용하는 함수
    • 특정 JPQL에서 네이티브 SQL 함수를 호출할 수 있다.
    • Hibernate는 DB 방언에 각 DB에 종속적 함수를 정의했다. 그리고 직접 호출할 함수를 정의하기도 가능하다.
  • 특정 DB만 지원하는 힌트
    • Hibernate를 포함한 몇몇 JPA구현체가 지원한다.
  • 인라인 뷰(from절 서브쿼리), UNION, INTERSECT
    • JPA구현체들이 지원한다. Hibernate는 지원 ❌
  • 스토어드 프로시저
    • JPQL에서 호출 가능
  • 특정 DB만 지원하는 문법
    • 너무 유니크한 쿼리 문법은 지원하지 않는데, 이때 네이티브 SQL사용. 근데 이걸 사용할까...?

네이티브 SQL을 사용하면 엔티티를 조회할 수 있고, JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.

네이티브 SQL 사용

네이티브 쿼리 API는 3가지가 있다.

  • 결과 타입을 정의
  • 결과 타입을 정의할수 없을 때
  • 결과 매핑 사용

엔티티 조회

    @Test
    @DisplayName("네이티브 SQL 엔티티 조회")
    void nativeQueryTest() {
        String sql = "select id, name, price from item where price > ?";
        Query nativeQuery = em.createNativeQuery(sql, Item.class).setParameter(1, 100);

        List<Item> items = nativeQuery.getResultList();
    }

jdbc 사용할때와 똑같은 느낌이 든다.
근데 가장 중요한점은
SQL만 직접 사용할 뿐, JPQL을 사용할 때와 같다. 조회한 엔티티도 영속성 컨텍스트에서 관리 된다.

값 조회

값으로 조회하려면 엔티티 조회처럼 class를 같이 넣어주는게 아니라
em.createNativeQuery(sql) 를 사용하면 된다.
대신 이때 nativeQuery.getResultList() 는 Object 배열을 반환하므로
List<Object[]> 로 반환을 받아야한다.
더욱 더 JDBC같이 생겼다.

결과 매핑 사용

결과 매핑을 사용하면 엔티티 자체에 너무 많은 어노테이션 설정을 해야되므로 보편적으로 사용하지 않을 것 같다는 나의 생각이 들어있다.
그래도 정리를 해보도록 하겠다.

String sql = "select M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT FROM MEMBER M " +
"LEFT JOIN (SELECT IM.ID, COUNT(*) AS ORDER_COUNT FROM ORDERS O, MEMBER IM " +
"WHERE O.MEMBER_ID = IM.ID) I ON M.ID = I.ID";

Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");
List<Object[]> members = nativeQuery.getResultList();

아래는 매핑 정의 코드이다.

@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
    entities = {@EntityResult(entityClass = Member.class) },
    columns = {@ColumnResult(name = "ORDER_COUNT")}
}
public class Member {...}

id, age, name, team_idMember 엔티티로 매핑을 시키고 order_count 는 단순 칼럼으로 매핑했다.
이렇게 여러 컬럼들을 매핑해서 추출할 수 있다.

@NamedNativeQuery

Named 네이티브 SQL을 사용하여 정적 SQL도 작성이 가능하다.
엔티티 클래스에

@NamedNativeQuery(
    name = "Member.memberSQL",
    query = "select 조회 쿼리문",
    resultClass = Member.class
)

로 등록해주고 사용하고자 하는 곳에서

TypedQuery<Member> nativeQuery = em.createNamedQuery("@NamedNativeQuery의 name", Member.class)
//파라미터가 있을 때
.setParameter();

네이티브 SQL은 휴먼에러를 발생할 확률이 QueryDSL보다 굉장히 높을 것으로 예상한다.
그래서 웬만하면 QueryDSL로 하지만 한방쿼리가 적절하게 필요할때만 사용하도록 해야할듯? 싶다. 😅
아직 실무에서 제대로 사용하지 않아서 이런 실무에서의 타협점은 점차 늘려가야 될것으로 보인다.

728x90

'JPA' 카테고리의 다른 글

JPA metamodel must not be empty!  (0) 2022.08.06
[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Querydsl 2  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
728x90

이번 포스팅에서는 조인에 대해 알아볼 것이다.

조인

조인은 innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 fetchJoin을 사용할 수 있다.

jpaQueryFactory.selectFrom(item).join(조인할 쿼리클래스).fetch();

연관관계가 있으면 그냥 join만 사용해도 되지만 지금은 on이 추가되어 on() 절로 연관관계 없이 조인도 가능하다.

jpaQueryFactory.selectFrom(item).join(chair).on(chair.name.eq(item.name)).fetch();


from절에 여러 조건을 사용해서 세타조인도 가능하다.

서브 쿼리

서브 쿼리는 예전 버전에서는 JPASubQuery를 사용했지만 업데이트가 되어 현재 버전에서는 JPAExpressions 를 사용하여 서브쿼리를 작성한다.

@Test
@DisplayName("subQuery 테스트")
void subQueryTest() {
    jpaQueryFactory.selectFrom(item)
                   .where(item.name.eq(String.valueOf(JPAExpressions.selectFrom(chair).where(chair.name.eq("item3")))))
                   .fetch();
}

전 직장에서 근무하였을때 서브쿼리로 너무 많은것들을 처리했던 레거시 쿼리가 상당히 많아서
서브쿼리를 사용할때는 항상 생각해보고 쿼리를 짜야 좋을것 같다.
이게 다른 조회를 여러번 하는것이나 Join을 사용할때 보다 느린것 같다. 자세하게 속도를 비교해보면서 포스팅을 한번 했었어야 했는데 이게 안되서 개인적으로는 아쉬운 부분이다.😅

프로젝션

프로젝션 대상이 하나라면 해당하는 타입으로 결과를 받아야 한다.

List<String> list = jpaQueryFactory.select(item.name).from(item).orderBy(item.name.desc()).fetch();

그렇지만 대상이 여러개라면?
반환값이 Tuple인 객체를 반환하기 때문에 이렇게 사용해야 한다.

List<Tuple> list = jpaQueryFactory.select(item.name, item.price).from(item).orderBy(item.name.desc()).fetch();

가만보니까 이렇게 특정 칼럼만 받아서 쓰는 서비스 로직이 따로 있을거란 생각이 든다.
그때 그냥 DTO를 써주면 어떨까?

빈 생성

  • 프로퍼티 접근(Setter)
  • 필드 접근
  • 생성자 사용
    객체를 생성하는 방법 3가지 이다. 이것을 통해 그리고 Projections를 사용하여 dto객체를 생성해주면 될듯 하다.

주의❗️ 빈 생성자를 무조건적으로 넣어줘야 한다.

기본값이 빈 생성자라서 명시적으로 넣어주지 않았는데
다음과 같은 에러가 발생한다. 😱


protected 생성자 넣었을때 또 protected 에러를 발생시킨다.

그래서 lombok의 @NoArgsConstructor를 넣어주었더니 정상 실행이 된다.
public 접근제어자여야 한다.

@Test
@DisplayName("projection dto")
void dtoTest() {
        List<ItemDto> result = jpaQueryFactory.select(
        Projections.bean(ItemDto.class,item.name.as("name"), item.price))
        .from(item).fetch();
}

프로퍼티 접근 방식인데 여기선 Projections.bean을 사용한다. 이것이 setter 메소드를 사용해서 값을 채우는 방식이다.

필드 직접 접근 방식은 Projections.fields()를 사용하면 된다.

생성자 접근 방식은 Projections.constructor()를 사용하는데 지정한 프로젝션과 파라미터 순서가 같은 생성자여야 잘 동작한다.

수정, 삭제 배치 쿼리

이부분은 그냥 영속성 컨텍스트에서 삭제, 수정 해주는것이 더 편리할것 같아서 따로 정리하지는 않고 읽는 단계로 넘어가겠다.

동적 쿼리

여기가 내가 많이 생각했고 어떻게 해야 조건문으로 쿼리를 처리할지에 대한 고민을 했던게 이 부분인것 같다.

@Test
@DisplayName("동적 쿼리")
void 동적쿼리_Test() {
    SearchParam param = new SearchParam();
    param.setName(null);
    param.setPrice(200);

    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (!ObjectUtils.isEmpty(param.getName())) {
        System.out.println(param.getName());
        booleanBuilder.and(item.name.eq(param.getName()));
    }

    if (!ObjectUtils.isEmpty(param.getPrice())) {
        System.out.println(param.getPrice());
        booleanBuilder.and(item.price.eq(param.getPrice()));
    }

    List<Item> result = jpaQueryFactory.selectFrom(item).where(booleanBuilder).fetch();
}

BooleanBuilder를 사용하여 조건에따른 조건을 할당해서 조회해줄수가 있다.
이것을 할줄 몰라서 Mybatis를 고집했던 이유도 없지않아 있는것 같다.

JPQL이 기본인 CRUD보다 훨씬 중요하고 이것을 알아야 원하는대로 select 쿼리를 날려줄 수가 있다.
그러면서 동시에 이 QueryDSL을 쿼리로 만드는게 복잡하고 어렵다고 생각해서 mybatis에서 또는 그냥 @Query에서 못벗어난? 것 같다. 😭

여기에 리팩토링을 추가한다면 조건문 하나당 로직을 메서드로 분리하여 수행해주면 편할것이다.

728x90

'JPA' 카테고리의 다른 글

[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Native SQL  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - 3  (0) 2022.08.05
728x90

QueryDSL

Criteria의 단점 너무 복잡하고 어렵다는 것 그래서 JPQL이 어떻게 생성되는지 파악이 어렵다.
그래서 나온게 이 QueryDSL이다.. 코드로 작성하는데 간결하고 알아보기 쉽다.

QueryDSL은 오픈소스 프로젝트이다. 단순 CRUD보다는 이름에 걸맞게 데이터를 조회 그러니까 통계형 쿼리를 짤때 적합하지 않을까 생각한다.

QueryDSL Setting

build.gradle

buildscript {
    ext {
        ...
        querydslVersion = '1.0.10'
    }

    dependencies {
        ....
        classpath "gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:$querydslVersion"
    }
}

subprojects {
    apply plugin: 'com.ewerk.gradle.plugins.querydsl'

    ext {
        querydslDir = "$buildDir/generated/querydsl"
    }

    dependencies {
        ...
        implementation 'com.querydsl:querydsl-jpa'
    }

    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }

    sourceSets {
        main.java.srcDir querydslDir
    }

    configurations {
        querydsl.extendsFrom compileClasspath
    }

    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
}

이렇게 설정을 해주었는데 중요한 부분은 subproject.ext.querydslDir 부분이다.
$buildDir이 뜻하는 것은 우리의 스터디는 일단 모듈을 나누어서 한사람당 모듈을 사용하고 있다.
그래서 의존성을 구분해놓았는데 여기서의 $buildDir은 모듈의 빌드된 폴더
build/를 의미하며 build/generated/querydsl 폴더에 Entity클래스 앞에 Q가 붙은 클래스가 빌드되어 있다.
이것으로 QueryDSL을 세팅해주는 것이다.

QuerydslConfig.java

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

전에 말했듯 JavaEE환경에서는 @PersistenceContext를 활성화하면 알아서 주입받는다.
JPAQueryFactory 가 QueryDSL을 사용하기 위해서 구현해야 하는 것이다.

이렇게 설정해주면 QueryDSL을 사용할 수가 있다.

이제 테스트 코드를 작성해 보자.

@DataJpaTest
public class QuerydslTest {
    @PersistenceUnit
    EntityManagerFactory emf;
    EntityManager em;
    EntityTransaction tx;
    JPAQueryFactory jpaQueryFactory;

    private Member member;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        jpaQueryFactory = new JPAQueryFactory(em);
        tx.begin();
        Address address = Address.builder()
                .city("city")
                .street("street")
                .zipcode("zipcode")
                .build();

        Address comAddress = Address.builder()
                .city("cocity")
                .street("costreet")
                .zipcode("cozipcode")
                .build();

        Period period = Period.of("20210714", "20210714");

        member = Member.builder()
                .name("kim")
                .age(26)
                .period(period)
                .homeAddress(address)
                .companyAddress(comAddress)
                .build();

        em.persist(member);
        em.flush();
    }

    @Test
    @DisplayName("QueryDSL 시작")
    void querydslTest() {
        QMember qMember = new QMember("m"); //생성된 별칭
        List<Member> members = jpaQueryFactory.selectFrom(qMember)
                .where(qMember.name.eq("kim")).orderBy(qMember.id.desc()).fetch();

        assertThat(members.get(0).getName()).isEqualTo("kim");
        assertThat(members.get(0).getAge()).isEqualTo(26);
    }
}

책에서는 orderBy(qMember.id.desc()).list(qMember); 로 사용했는데
버전이 바뀌면서 list 메소드는 없어졌다고 한다.
그래서 공식문서를 찾아보니까

QueryDSL로 select 조회 쿼리를 만들었을때 로그이다.

이상한점 이라고 느낄수 있다면 어? 하고 코드상에는 jpaQueryFactory.from으로 시작하기 때문에 이것이 어떤 CRUD인지 모를 수 있다.fetch()를 해주면 반환이 List인데 그래도 타입은 캐스팅을 해주어야 한다.
자동으로 뭔가 Q클래스를 맞춰줄줄 알았다.

🤣

ㅋㅋㅋㅋㅋㅋㅋ 아니었다..... 그냥 기본값이 select가 아니라 list(qMember) 를 해주던 버전에서는 이 list 메소드가 엔티티 타입을 맞춰서 List에 넣어주었는데 지금은 selectFrom(qMember)를 해주면 이것이 List 타입을 맞춰준다.
이렇게 또 하나 깨달음을 얻는다 👍

페이징 정렬

@Test
void 페이징_테스트() {
    QItem item = QItem.item;
    List<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                                           .orderBy(item.price.desc(), item.name.desc())
                                           .offset(1).limit(3).fetch();

    result.forEach(r -> System.out.println(r.toString()));
}

여기서 where 조건에 lt는 부등호로 < 이고, gt는 >이다. orderBy절에서는 쿼리 타입인(Q)에서 asc(), desc()를 지원해준다. 페이징은 offsetlimit 을 조합해서 사용하면 된다.
이렇게해서 얻은 결과이다.

전체 데이터 수를 알고 싶을때는

listResults()를 사용한다.

이것 역시 바뀌었다.
SearchResults<T> 가 아니라 버전이 바뀌면서 QueryResults<T>로 변경되었다.

listResults() 가 아니라 fetchResults()로 바뀌게 되었다.

@Test
@DisplayName("조회 결과 테스트")
void listResultsTest() {
    QueryResults<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                .orderBy(item.price.desc(), item.name.desc())
                .offset(1).limit(3).fetchResults();

    long total = result.getTotal(); //500보다 작은수 총 count
    long limit = result.getLimit(); //limit 3
    long offset = result.getOffset(); // offset 1
    List<Item> results = result.getResults();

    assertThat(total).isEqualTo(4L);
    assertThat(limit).isEqualTo(3L);
    assertThat(offset).isEqualTo(1L);

    results.forEach(r -> System.out.println(r.toString()));
}

getTotal은 오류의 여지가 있어보인다. 왜냐면 저 QueryDSL 전체를 시켰을때의 count가 아니라 count가 먼저 실행되기 때문에 where() 조건까지 수행한 count가 나온다.

결과의 조회는 QueryResultsgetResults()를 사용하여 페이징 정렬할때처럼의 결과를 얻을수가 있다.

그룹

그룹은 groupBy를 사용하고 그 다음에 조건을 해주려면 having을 사용하면 된다.

List<Item> results = jpaQueryFactory.selectFrom(item)
                                    .groupBy(item.price)
                                    .having(item.price.lt(500))
                                    .fetch();

728x90
728x90

내 팀 프로젝트를 예전에 Spring으로 구현을 했었는데 이것을 스프링부트로 마이그레이션 해보았다. 더불어 Maven 의존성을 Gradle로 바꾸면서 마이그레이션을 한 것이다.

오류들이 상당히 많았지만 남들과 같은 오류인지는 잘 모르겠다.🤣

어떤 오류들이 있었는지 알아보자

Lombok

일단 Jar로 빌드하게되면 SpringBoot는 WEB-INF안의 jsp파일을 읽을수가 없다. 그래서 war로 빌드를 해주어야한다.

추후에 Mybatis로 db연결을 하는 방식을 JPA로 변경할 예정이다.

우선 제일 먼저 gradle 을 의존성 관리 툴로 추가하기 위해서

pom.xml 이 있는 경로로 가서 gradle init을 시켜주었다.

gradle init --type pom

이것으로 pom.xml의 내용이 Gradle로 변환이된다.

그다음 war파일이 있어야 실행할 수 있는 환경이 되기 때문에

apply plugin: 'war'

를 해준다.
이왕이면 최신버전으로 마이그레이션 하자 생각해서 다 최신버전으로 엮어주었다.

여기서 에러가 발생했던건 lombok이었는데
implementaion만으로 lombok을 추가하는 것이 아니라
annotationProcessor로도 lombok을 추가해줘야 했었다.

간략하게 gradle 지시어를 정리하자면

  • compileOnly: 해당 의존성을 컴파일시에만 포함한다.

  • runtimeOnly: 해당 의존성을 런타임시에만 포함한다.

  • compile: 해당 의존성을 직/간접적으로 의존하고 있는 모든 의존성을 재빌드한다.

  • implementation: 해당 의존성을 직접 의존하고 있는 의존성만 재빌드 한다.

기본적으로 포함이 되어있지 않은 어노테이션이 바로 lombok이기 때문에
annotationProcessor로 명시적 추가를 해줘야한다.

Mybatis

탈 xml을 하기 위해서 이것도 다 Java의 @Bean으로 설정을 해주었다.
여기서도 에러가 조금 많이 발생했다.

@Configuration
@MapperScan(
        sqlSessionFactoryRef="dataSource",
        sqlSessionTemplateRef="sqlSessionFactoryBean")
public class MapperConfig {
    @Value("${spring.datasource.driver-class-name}")
    String driverClassName;

    @Value("${spring.datasource.url}")
    String url;

    @Value("${spring.datasource.username}")
    String userName;

    @Value("${spring.datasource.password}")
    String password;

    @Bean(name="dataSource")
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean(name="sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setVfs(SpringBootVFS.class);

        sessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis/Configuration.xml"));
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sessionFactoryBean;
    }
    @Bean(name="sqlSessionTemplate")
    public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

application.properties

여기에는 web.xml을 제거하고 application.properties에

server.port=8080
server.servlet.context-path=/

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
spring.mvc.static-path-pattern=/resources/static/**

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/테이블명
spring.datasource.username=계정명
spring.datasource.password=비밀번호

이렇게 진행했다. 그래서 파일 구조도 resources의 static 안으로 js, css 등 여러 정적 파일들을 넣어주고 불러오는 것으로 경로를 잡았다.

그러면서 동시에 WEB-INF폴더는 src/main/webapp 하위에 넣어주고 밖으로 빠져있던 jsp파일도 안으로 넣어주었다.

Tomcat

org.apache.catalina.webresources.Cache.getResource []에 위치한 리소스를 웹 애플리케이션 []을(를) 위한 캐시에 추가할 수 없습니다.
이런 에러도 Tomcat 구동시에 에러를 뱉었는데
이것은
tomcat경로중 apache-tomcat/conf/context.xml
아래와 같은 문구를 넣어준다.

<Resources cachingAllowed="true" cacheMaxSize="100000" />

이렇게 보면 단순한 에러들이었는데 Mybatis가 얽히고 조금만 건드리면 바로 에러가 나고 마이그레이션 경험이 생겨서 좋은것 같다. 앞으로도 레거시를 업데이트 해야할 경우에 오류들을 상기시키면서 잘 진행해보면 한번 더 성장할 수 있을것 같다.

728x90

'Spring' 카테고리의 다른 글

Spring -> Spring Boot 마이그레이션 2  (0) 2022.08.06
Spring Data JPA  (0) 2022.08.06
[Spring] MockMvc Bean 주입 에러  (0) 2022.08.04
[Spring] Spring Security  (0) 2022.08.03
728x90

옵티마이저(Optimizer)

옵티마이저란, SQL을 가장 빠르고 효율적으로 수행할 최적(최저비용)의 처리 경로를 생성해주는 데이터베이스의 핵심엔진이다.

여기서 사용한 DB는 MariaDB이다.

옵티마이저 엔진

  • Parser : SQL 문장의 각 요소를 파싱해서 파싱트리를 만듦(문법 검사, 구문 분석)
  • Query Transformer : SQL문을 효율적으로 실행하기 위해 더 일반적이고 표준적인 형태로 옵티마이저 변환
  • Estimator : 시스템 통계정보를 사용해서 SQL 실행비용을 계산
  • Plan Generator : SQL을 실행할 계획들을 수립
  • Row-Source Generator : 옵티마이저가 생성한 계획을 SQL Engine이 실행 가능한 코드로 포맷
  • SQL Engine : SQL 실행

컴퓨터의 핵심이 CPU인 것처럼 DBMS의 핵심은 옵티마이저라고 할 수 있다. 우리가 SQL을 작성하고 실행하면 소프트웨어 실행파일처럼 즉시 실행되는 것이 아니라 옵티마이저(Optimizer)라는 곳에서 어떤 동작으로 실행할지 여러 가지 실행계획을 세우게 된다. 이렇게 실행계획을 세운 뒤 시스템 통계정보를 활용하여 각 실행계획의 예상 비용을 산정한 후 각 실행계획을 비교해서 최고의 효율을 가지고 있는 실행계획을 판별한 후 그 실행계획에 따라 쿼리를 수행하게 되는 것입니다.

옵티마이저가 선택한 실행 방법의 적절성 여부는 쿼리 수행 속도에 가장 큰 영향을 미친다.

옵티마이저 종류

규칙기반 옵티마이저

미리 정해놓은 규칙(액세스 경로별 우선순위)에 따라 액세스 경로를 평가하고 실행계획을 선택함.

비용기반 옵티마이저

예상되는 비용(쿼리 수행에 필요한 시간)을 기반으로 최적화를 수행한다. 미리 구한 테이블과 인덱스에 대한 통계정보를 기초로 각 오퍼레이션 단계별 예상 비용을 산정하고, 총비용이 가장 낮은 계획을 선택한다.(부적절한 통계정보의 경우 성능 저하 우려)

우리는 이 비용기반 옵티마이저를 주로 사용할 것 같다.

옵티마이저 특징

  • 서론에서 말했듯이, 시스템 통계 정보를 사용해서 예상되는 비용을 산정하고 최저비용 가지고 있는 계획을 선택해서 SQL을 실행한다.
  • 옵티마이저는 자동으로 하다보면 비효율적으로 실행 계획을 구성할 수가 있는데 사용자는 힌트(HINT)를 줌으로써 실행 계획을 변경할 수 있다.

실행계획 확인하는 방법

실행계획을 확인하는 것은 explain을 쿼리문 앞에 붙여주면 된다.
explain select * from posts 를 실행해 보았다.

실행계획은 이렇게 나온다.

id컬럼은 쿼리별로 부여되는 식별자.

select_type컬럼 은 기본 SELECT를 실행하면 SIMPLE이 나오는데
PRIMARY는 UNION이나 서브쿼리가 포함된 SELECT 쿼리의 실행 계획중 가장 바깥의 단위 쿼리는 PRIMARY로 나온다.

type컬럼은 SQL서버가 테이블의 레코드를 어떻게 읽었는가를 알려주는 지표이다. 방식은 사용자 정의 인덱스를 활용하여 읽었는지, (기본값)테이블 풀 스캔으로 읽었는지에 대한 결과가 나온다.

  • All : 다른 접근방법으로 처리할 수 없을 때 사용하는 마지막 선택이기 때문에 가장 비효율적 방법이다.
  • index : 인덱스를 활용해 읽었을 경우 표시됨. 테이블 풀 스캔보다 빠르다.

이 옵티마이저를 튜닝하려고 할 때 인덱스를 많이 사용하는데 필요한 곳에만 인덱스를 걸어주도록 하자.
무의미하게 인덱스를 걸면 오히려 역효과를 초래할 수가 있다.

인덱스를 줄 때는 카디널리티가 높은 칼럼을 매칭하여 인덱스를 할당해야 한다.
카디널리티가 높다는 것은 총 row수 * 선택도이다.
그러니까 다시말하자면, 고를 수 있는 조건이 많은 칼럼을 주는 것이다.

성별 이름 주민번호
홍길동 team1 950222-1
아무개 team2 951231-2
홍길동 team3 960805-1
...

이런식으로 테이블이 있다고 가정할 때 선택도가 가장 많은 칼럼은 당연히 주민번호 일 것이다. 왜냐면 고유한 정보이므로 카디널리티가 다른 칼럼보다 월등히 높다. 이런 칼럼에 인덱스를 부여하면 옵티마이저를 튜닝함으로써 쿼리문을 엄청 빠르게 수행할 수 있게 도와줄 것이다.

728x90

'DB' 카테고리의 다른 글

쿼리 속도 개선기  (0) 2022.08.07

+ Recent posts