[Spring/JPA] - max 엔티티 추출하기

june·2022년 12월 12일
0
post-thumbnail

실무에서 해결해야할 로직이 있었는데 매우 쉽게 풀려서, 공유하고자 작성한다.

🥊 예시 - 펀치 머신

회원제 서비스로 돌아가는 펀치 머신이 있다고 가정하자.

그래서 우리는 회원이 자신의 정보를 기계에 인증하면, 회원의 최고 스코어를 뽑는 로직을 짜고싶다.

일단 회원의 스코어 히스토리만 구현한다 치고, MYSQL로 테이블을 구현하면 이런식이 될 것이다.

create table punch_history (
       id bigint not null,
        member_id varchar(255),
        score integer,
        primary key (id)
    )

스프링에서 엔티티로 구현하면 이런식이다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PunchHistory {
    @Id
    @GeneratedValue
    private long id;

    @Column
    private String memberId;

    @Column
    private int score;

    public PunchHistory(String memberId, int score) {
        this.memberId = memberId;
        this.score = score;
    }

    public int getScore() {
        return score;
    }

    public long getId() {
        return id;
    }
}

이 상황에서 sql로 해당 데이터를 추출하는 방법은 아래와 같다. 여기서는 A의 최대값 row를 뽑는다 가정해보자.

SELECT * FROM punch_history
	WHERE score = (SELECT MAX(score) FROM punch_history WHERE member_id = "A");

이제 우리는 이걸 Spring Boot 환경에서 JPA를 통해 구현하면 된다.

난 이걸 JPA에서 구현하는 방법, 총 3개가 생각났다.

  1. @Query를 통해 JPQL로 쿼리를 직접 작성해 구하기
  2. QueryDsl 사용
  3. 해당 회원의 전체 히스토리를 뽑아서 가장 score가 높은 값을 추출

1번을 하기에는 JPQL이 조금만 복잡해져도 코드가 더러워져서 구현하기 싫었고, 그렇다고 2번을 사용하자니 쿼리 하나 때문에 gradle부터 설정을 복잡하게 세팅하며 충돌 방지하는게 귀찮았다.

그래서 3번으로 시도를 했다.

@Repository
public interface PunchHistoryRepository extends JpaRepository<PunchHistory, Long> {
    List<PunchHistory> findAllByMemberId(String memberId);
}

먼저 JpaRepository를 확장하여 Repository를 구현한 후, JpaRepository의 기능을 이용해, memberId로 모든 엔티티를 검색해오는 로직을 구현했다.

이제 테스트문을 작성해보자.

@Slf4j
@SpringBootTest
public class JpaTests {

    @Autowired
    PunchHistoryRepository repository;

    @Test
    public void howToFindMax() throws Exception {
        //given
        PunchHistory ph1 = new PunchHistory("A", 500);
        PunchHistory ph2 = new PunchHistory("B", 600);
        PunchHistory ph3 = new PunchHistory("C", 700);
        PunchHistory ph4 = new PunchHistory("A", 990);
        PunchHistory ph5 = new PunchHistory("B", 550);
        PunchHistory ph6 = new PunchHistory("A", 999);


        List<PunchHistory> punchHistories = Arrays.asList(ph1, ph2, ph3, ph4, ph5, ph6);
        repository.saveAll(punchHistories);


        //when
        List<PunchHistory> aPunchList = repository.findAllByMemberId("A");

        if (aPunchList.size() == 0) {
            throw new RuntimeException("A의 기록이 없습니다");
        }
        PunchHistory aMaxPunchHistory = Collections.max(
                aPunchList, Comparator.comparingInt(PunchHistory::getScore)
        );

        //then
        Assertions.assertEquals(999, aMaxPunchHistory.getScore());

    }
}

각각 다른 punch_history들을 DB안에 넣고, 앞서 구현한 findAllByMemberId메소드로, 해당하는 PunchHistory 엔티티 리스트를 불러온다.

이후 리스트의 크기가 0이면 예외처리하고, 그게 아니면, CollecionsComparator의 기능을 활용해, 안의 객체들의 값을 서로 비교하여 가장 큰 값을 뽑는다.

이 후, 해당하는 값의 숫자와 가장 큰 엔티티인 aMaxPunchHistory의 스코어가 같은지 비교한다.

결과는 성공적으로 진행이 된다.

하지만 이 로직은 문제점이 있다.

첫 번째는 모든 A의 히스토리를 다 뽑아온다는 점에 있어서 문제고, 두번째는 거기서 또 모든 값을 비교해서 max를 뽑아낸다는게 문제다.

지금이야 값이 총 6개밖에 안되니까 충분히 문제없이 돌릴 수 있지만, 만약 A의 기록이 1000개, 10000개, 혹은 그 이상이라면?

우리는 총 10000개의 리스트를 뽑아온 다음, 그 리스트의 객체들을 다시 일일이 비교를 해서 최대값을 뽑아야한다.

이는 매우 비효율적인 로직이며, 성능저하를 불러일으킬 수 있다.

🔝 Spring Data JPA의 Top 기능

그래서 이걸 어떻게 효과적으로 구현할 수 있을까 고민하던 찰나에, JpaRepository, 즉 Spring Data JPA의 메서드 생성 기능을 통해 구현할 수 있음을 깨달았다.

바로 Top 키워드를 이용하는 것이었다.

이를 Repository를 통해 구현하면 이런 메서드가 된다.

@Repository
public interface PunchHistoryRepository extends JpaRepository<PunchHistory, Long> {
    Optional<PunchHistory> findTopByMemberIdOrderByScoreDesc(String memberId);
}

이제 테스트를 해보자.

    @Test
    public void howToFindMaxByTop() throws Exception {
        //given
        
        //when
        PunchHistory a = repository.findTopByMemberIdOrderByScoreDesc("A").orElseThrow(RuntimeException::new);

        //then
        Assertions.assertEquals(999, a.getScore());
        
    }

성공적으로 진행이 된다.

그러면 쿼리를 어떻게 날리길래 이런 로직이 실행이 되는 걸까?

로그를 보자.

처음에 중첩 SELECT 문을 써서 조회했던 것과 달리, Spring Data JPAwhere 조건문에 아이디를 넣고 order byDESC로 구현한 다음(스코어가 높은 것을 가져와야하니까!) limit를 1로 걸어 단 한가지만 가져온 것이다!

즉 이걸 풀어서 SQL문으로 쓰면 이렇게 된다!

select * from punch_history ph WHERE member_id = "A" order by score DESC limit 1;
profile
초보 개발자

0개의 댓글