본문 바로가기

Spring/JPA

[Spring Data JPA] Specification

이번 장에서는 명세(Specification)에 대해서 알아본다.
글의 하단부에 참고한 강의와 공식문서의 경로를 첨부하였으므로 자세한 사항은 강의나 공식문서에서 확인한다.
모든 코드는 깃허브 (링크)에 있다.


Domain Driven Design (DDD)에는 명세(Specification)라는 개념이 있다.
Spring Data JPA에는 JPA Criteria를 활용하여 명세라는 개념을 사용할 수 있도록 지원한다.

참고한 강의에서 실무에서는 사용되지 않는 기능이라고 소개하고 있다.
Specification을 사용하여 개발을 진행하지는 않겠지만 언제 이러한 코드를 리팩토링하게 될지 모르니 자세히 알아보도록 한다.

JpaRepository가 JpaSpecificationExecutor를 상속받도록 한다.

SoccerPlayerDataRepository

public interface SoccerPlayerDataRepository extends
        JpaRepository<SoccerPlayer, Long>,
        SoccerPlayerDataRepositoryCustom,
        JpaSpecificationExecutor<SoccerPlayer> {
        // 생략
        }

Specification을 위한 클래스를 생성한다.
필자의 경우 팀의 이름으로 검색하는 teamName(),
입력받은 값보다 큰 선수를 검색하는 greaterHeight(),
입력받은 값보다 무거은 선수를 검색하는 greaterWeight()

세 개의 메서드를 구현하였다.

SoccerPlayerSpecification

public class SoccerPlayerSpecification {

    public static Specification<SoccerPlayer> teamName(final String name) {
        return (root, query, criteriaBuilder) -> {
            if (!StringUtils.hasText(name)) {
                return null;
            }
            Join<SoccerPlayer, Team> team = root.join("team", JoinType.INNER);
            return criteriaBuilder.equal(team.get("name"), name);
        };
    }

    public static Specification<SoccerPlayer> greaterHeight(final int height) {
        return ((root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get("height"), height));
    }

    public static Specification<SoccerPlayer> greaterWeight(final int weight) {
        return ((root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get("weight"), weight));
    }

}

Specification을 사용하는 코드는 아래와 같다.
Specification을 위한 클래스에서 정의한 메서드를 조립(컴포지트 패턴)하여 원하는 검색 조건을 만든다.

@Transactional
@SpringBootTest
class SoccerPlayerDataRepositoryTest {

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private SoccerPlayerDataRepository dataRepository;

    @Test
    @DisplayName("Specification 테스트")
    void specificationTest() {
        Team team1 = new Team("TeamA");
        Team team2 = new Team("TeamB");
        List<SoccerPlayer> players = List.of(
                new SoccerPlayer("Roy", 173, 73, team1),
                new SoccerPlayer("Perry", 180, 80, team1),
                new SoccerPlayer("Sally", 160, 50, team1),
                new SoccerPlayer("Dice", 183, 90, team2),
                new SoccerPlayer("Louis", 178, 85, team2)
        );
        dataRepository.saveAll(players);
        entityManager.flush();
        entityManager.clear();

        Specification<SoccerPlayer> specification =
                SoccerPlayerSpecification.teamName("TeamA")
                        .and(SoccerPlayerSpecification.greaterHeight(170))
                        .and(SoccerPlayerSpecification.greaterWeight(70));

        List<SoccerPlayer> storedPlayers = dataRepository.findAll(specification);
        assertEquals(2, storedPlayers.size());
    }
}

테스트는 통과할 것이며 발생한 쿼리는 아래와 같다.

Hibernate: 
    select
        soccerplay0_.soccer_player_id as soccer_p1_1_,
        soccerplay0_.created_at as created_2_1_,
        soccerplay0_.updated_at as updated_3_1_,
        soccerplay0_.created_by as created_4_1_,
        soccerplay0_.updated_by as updated_5_1_,
        soccerplay0_.height as height6_1_,
        soccerplay0_.name as name7_1_,
        soccerplay0_.team_id as team_id9_1_,
        soccerplay0_.weight as weight8_1_ 
    from
        soccer_player soccerplay0_ 
    inner join
        team team1_ 
            on soccerplay0_.team_id=team1_.team_id 
    where
        team1_.name=? 
        and soccerplay0_.height>170 
        and soccerplay0_.weight>70

우리가 예상한 쿼리가 발생하였다.
또한 명세를 chaining할 때 and 이외에 where(), or(), not()도 사용이 가능하다.


참고한 강의:

JPA 공식 문서:

위키백과:

'Spring > JPA' 카테고리의 다른 글

[Spring Data JPA] Projections  (0) 2022.03.27
[Spring Data JPA] Query By Example  (0) 2022.03.27
[Spring Data JPA] isNew()  (0) 2022.03.27
[Spring Data JPA] Implementor  (0) 2022.03.27
[Spring Data JPA] Auditing  (0) 2022.03.26