[TIL] 최종 프로젝트 (6) - 연령대 키워드 커스텀 조회

J쭈디·2025년 2월 18일
0

Sparta_프로젝트

목록 보기
17/35

1. 동시성 제어, 그 후

동시성을 결국 낙관적 락으로 끝낼 거 같다는... 그런 결론이 나왔으나 결과를 보고 또 다시 팀 회의를 해본 결과 정합성을 위해서 비관적 락으로 하되, 성능을 개선하자는 이야기로 발전이 되었다.

오늘 오전까지 계속 그걸로 이것저것 찾아보는데 조회를 먼저 하고, 저장을 나중에 하는 방법으로 동시성을 어떻게 ... 데드락을 덜 걸리게 하는 방법이 있지 않을까? 하는 생각으로 일단 비관적 락 구현하고 테스트한 걸 기본으로 머지하는데 성공했다.

그리고... 동시성 제어가 끝나는 대로 연령대별 인기 키워드 조회와 연령대별 인기 즐겨찾기 조회를 만들어야 했다. 왜냐하면 오늘 밤까지 만들기로 내가 먼저 내 입으로 말했기 때문이다.

오, 생각보다 빡센데 이거? 원래는 20대부터 70대까지 상위 10개를 모두 조회되게끔 설정했으나, 이게...채용공고에 나이대가 너무 스펙트럼이 넓다는 의견이 나왔다.

그래서 나는 나이를 입력받고 상위 10개 키워드와 즐겨찾기를 검색하는 기능을 개발하는 것으로 변경이 되었다. 동적쿼리 당 첨.... ㅎ

요 며칠 동시성만 하느라 별 거 한 게 없다고 생각되니 오히려 좋아(?) 일지도 모른다.

2. 커스텀연령대 인기키워드 조회

연령대를 커스터마이징하고, 인기있는 키워드를 상위 10개로 노출되는 그런 것을 시도해보았는데... QueryDSL 하자마자 에러가 터졌다.

엥... 왜 예상치 못한 에러 발생이지..?? QureyDSL에 Projections을 잘못 적용했나..???

1. 에러, 에러, 또 에러!

처음에 나는 다른 팀원 분들이 열심히 만들어준 Dto 클래스들이 많아서(정확히는 recode) 그걸 활용하려고 했었다.
그래서 ReadKeywordResponseDto라는 recode를 그냥 활용해보기로 했었는데... 그 결과, 타입이 안 맞는다고 예상치 못하는 에러가 발생했다. ㅋㅋㅋㅋ

return jpaQueryFactory.select(
            Projections.constructor(
                ReadKeywordResponseDto.class,
                keyword.id,
                keyword.name
            )
        ).from(keyword)
            .where(
                user.age.between(minAge,maxAge)
            )
            .limit(10)
            .fetch();

recode는 기본 생성자를 생성할 수 없기 때문에 생기는 오류라고 한다.
Projections.constructor()를 쓰면서 순서도 맞춰줘야 한다고 한다.

그래서 그냥 새 class 하나 파서 dto를 만들어주고, 그 참에 키워드가 총 추가된 수도 확인해볼 생각으로 keywordCount도 하나 추가해줬다. 이거는 서브쿼리를 써서 Wildcard를 사용하여 count 해줬는데 또 에러가 발생했다. 또...

같은에러...ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

이번에는 뭐지 싶어서 보니 WildCard는 기본적으로 Long 형이라고 한다.

그것도 고쳤는데... 왜 또 같은에러가 나는거지... (멍청)

이번엔 정답이 여기 있었다. 내가 서브쿼리 조회할 때 쓴 거에 문제가 있었다.
select(Wildcard.count).from(keyword).where(keyword.id.eq(userKeyword.id))
user가 추가한 키워드에 대한 테이블에서 id를 가져왔던 건데.. where 절에서 실수로 식별id를 가져와버린거다... 키워드 id를 가져와야지..브아보..
당연히 식별id는 겹치는 수가 없으므로 문제가 발생......하나? 싶지만 실제로 우리는 식별자만 더미를 45개 추가하고 채용공고는 1만건이 추가되어 있기 때문에 문제가 발생 할 수 밖에 없다.

아, 그리고 바보같은 .. 그걸 깨달아버렸다. 아직 age 필드만 추가되고 더미에 age가 추가되지 않은 사실을 방금 깨달았다. ㅎㅎ....

멍청이.... 일단은 그래서 user에 int형인 경력년도를 기준으로 해서 임시로나마 검색을 시도해봤고, 여전히 안 되었다!

결국 GPT를 호출해봤더니 QueryDSL을 사용할 때는 연관관계가 있다 하더라도 join이 필요하댄다...ㅎㅎㅎ 그냥 생짜로 keyword만 from 하고 있었으니 이모양이지...

나는 userkeyword와 그 연관관계인 keyword 테이블, user 테이블까지 관련해서 검색해야 하므로 아래와 같은 형태로 변경하였다.

from(userKeyword)
            .join(userKeyword.keyword,keyword)
            .join(userKeyword.user,user)
            .where(
                user.career.between(minAge,maxAge)

결과, 성공적 ㅎㅎ

근데, 여기다 아무래도 count도 추가해주고, 뭔가 더 넣어주고 싶다는 생각이 자꾸만 든다....허허허... 그리고 더미를 추가해주면 더 정확하게 age에 따른 값이 들어갈 터이니...

2. 정렬아 되어라!!

근데 이번에는 정렬이 말썽이다.

카운트까지 구현했는데 그 탓에 정렬이 문제라는 걸 확실히 알 수 있었다.

정렬이 id를 기준으로 되는 거 같은데 뭐 굳이 귀찮으면 id와 count의 위치만 변경하면 되는 거 아닌가? 싶지만... 세상 일이 그리 쉬울 리가 있나... 일단 나는 솔직히, 언젠가는 이렇게 selec문을 이용해 서브쿼리 때리는 걸 정렬해야 할 일이 반드시 또 온다고 생각한다.

그리고 뭐 사실 별칭만 있으면 금방 아닌가..? 해서 별칭 만드는 법을 찾아봤다.
JPAExpressions를 사용하면 된다고 한다. 근데 찾아보니 하나같이 그냥 의미없이.. Q클래스에 별칭을 사용하고 있다...

나는 튜터님에게 들어서 아는 사실인데 Q클래스는 그냥.. 정적으로 import 해서 쓰면 된단다... 그럼 별칭을 적어서 선언을 왜 하겠나 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 무쓸모...까진 아니어도 그닥? 할 이유가 없다고 생각한다.

그리고... ExpressionUtils.as 라는 아이를 찾았다... 이거다..(후후)
근데 또 숫자타입에서 문제가 발생하고... 결국 GPT를 두드려보니 NumberExpression<Long> 타입으로 선언해서 ExpressionUtils.as 형태로 저장하면 된다고 알게되었다. 참 오래도 걸렸다.... ExpressionUtils.as는 물론 NumberExpression<Long> 형태로 캐스팅이 필수다. 안 그러면 에러가 난다.

응 아니야.. 다시 에러 발생이다. 캐스팅이 제대로 안 된 모양이다.
결국 나는 이 순간 별칭 사용을 포기했다...(눈물)

그리고 orderBy를 select문 통째로 가져오는 무식한 방법으로라도 광명을 찾을 수 있을 줄 알았는데......................결국 안된다..안되어버린다.

3. 정렬과의 전쟁인가 별칭과의 전쟁인가? 서브쿼리를 기준으로 정렬이 이리 어려워도 되는가.

결국 쿼리에 직접 때려넣는 방식으로 해결을 보긴 했는데....이거 너무 느리다..?

로딩까지 속도가 생각보다 너무 느려서 놀랐고, 그 부분은 개선이 필요해 보인다. 일단은 느리더라도 구현한 것에 의의를 두기로 했다...

어, 근데 아무리 봐도 나이를 바꿔서 실험해봤을 때 카운트 수가 안 변하는 걸 깨달았다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

메인쿼리에서는 between을 걸어놓고 막상 count에서는 안 걸어놔서 그 모양이었다.
처음 시작한 거에 비해서 너무 코드가 길어졌지만 아무튼 1차 얼레벌레 코드 완성이다.

근데 이번엔 또 그룹....count에까지 between 걸어놓으니 group by 에서 문제가 터졌다.

group by에서 문제가 터지지 않게 서브쿼리에 조인을 추가해서 조건절을 걸었더니 겨우 성공했다. ㅋㅋㅋㅋㅋㅋㅋ 생각보다 쉽지 않다 이거..

        return jpaQueryFactory.select(
                Projections.constructor(
                    KeywordCustomAgeResponseDto.class,
                    keyword.id,
                    keyword.name,
                    ExpressionUtils.as(
                        select(userKeyword.id.count())
                            .from(userKeyword)
                            .join(userKeyword.user, user)
                            .where(user.age.between(minAge, maxAge)
                                    .and(userKeyword.keyword.id.eq(keyword.id))),
                        "keywordCount")
                ))
            .from(userKeyword)
            .join(userKeyword.keyword, keyword)
            .join(userKeyword.user, user)
            .where(user.age.between(minAge, maxAge))
            .groupBy(keyword.id, keyword.name)
            .orderBy(
                Expressions.numberPath(Long.class, "keywordCount").desc()
            )
            .limit(10)
            .fetch();
    }

이제 즐겨찾기 하러 가야한다.

<출처>
https://sjh9708.tistory.com/174
https://sjh9708.tistory.com/180
https://web2eye.tistory.com/242

profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글