-> 블로그 이전

[Spring Data JPA] 쿼리 메소드 기능

2022. 8. 18. 20:16Language`/JPA

더보기

 ## 쿼리 메소드 기능을 테스트할 기본적인 엔티티 구성

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(name = "user_name", length = 50)
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private MemberType type;

    @Column(name = "enabled")
    private boolean enabled;

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

    @OneToMany(mappedBy = "member")
    private List<Order> orderList = new ArrayList<>();
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    private Integer orderAmount;

    @Embedded
    private Address address;

    private LocalDateTime orderDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    private Long id;

    @Column(name = "product_name", length = 50)
    private String name;

    private Integer price;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(name = "team_name", length = 50)
    private String name;

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

첫번째로 Spring Data JPA를 사용하면 "인터페이스"만으로도 엔티티에 대한 Repository를 정의할 수 있다

1. 엔티티에 대한 인터페이스 레포지토리를 만들면 "JDK 동적 프록시"에 의해서 인터페이스에 대한 구현 클래스를 동적으로 생성해준다. 따라서 개발자가 직접 구현 클래스를 만들지 않아도 JDK 동적 프록시에 의해서 인터페이스만으로도 DAO를 완벽하게 구현할 수 있게 된다

2. 기본적으로 인터페이스 레포지토리를 만들기 위해서는 "JpaRepository 인터페이스"를 implements해야 한다
→ extends JpaRepository(<엔티티>, <엔티티의 식별자 타입>)

3. JpaRepository는 [PagingAndSortingRepository ← CrudRepository ← Repository]를 차례대로 extends하고 있으므로 우리는 각각의 인터페이스의 메소드를 구현없이 전부 사용할 수 있다

4. 실제 JpaRepository의 메소드를 구현한 클래스는 SimpleJpaRepository이고 이 클래스 내부로 들어가면 각 메소드별로 동작 방식을 확인할 수 있다

 

두번째로 Spring Data JPA를 사용하면 "메소드의 이름"만으로도 Data JPA가 알아서 분석 후 JPQL을 생성해준다

물론 메소드명을 자기 마음대로 지으면 절대로 안되고 Data JPA에서 정해준 규칙에 따라서 작성해야 한다

 

1. 메소드 이름으로 쿼리 생성 (키워드별 JPQL)

기본적으로 조회를 위한 메소드를 생성할때는 "find[엔티티 이름]By → 접두사-키워드-필드이름"로 시작해야 한다

(find, delete, count, exists) + [엔티티 이름] + By + (필드 이름, 키워드, ...)

접두사
→ 조회 = find/read/query/get...By
→ 개수 = count...By [long]
→ 존재 = exists...By [boolean]
→ 삭제 = delete/remove...By [long]
키워드 = And, Or, LessThan, GreaterThan, ....
필드 이름 = Username, Birth, OrderDate, ....

 

(1) And, Or

And = 2개의 파라미터에 대해서 모두 만족하는 결과

// 메소드
List<Order> findByAddressCityAndOrderAmount(String city, int orderAmount);

// 사용
List<Order> and = orderRepository.findByAddressCityAndOrderAmount("cityA", 5);

위와 같이 "임베디드 값 타입 내부의 필드"에 대한 검색 조건을 적용해주려면 [<임베디드 클래스><임베디드 필드>]로 연속적으로 적어주거나 [<임베디드 클래스>_<임베디드 필드>]로 적어줘야 메소드에 대한 JPQL이 정상적으로 생성된다

City 필드가 Address라는 임베디드 값 타입의 필드로 존재하는데 만약에 City라는 조건을 줄 때 단지 findByCity로 주게된다면 JPA는 인식을 하지 못한다
따라서 findByAddressCity 혹은 findByAddress_City로 종속적인 필드의 구조를 전부 적어줘야 정상적으로 쿼리가 생성된다

 

Or = 2개의 파라미터에 대해서 하나라도 만족하는 결과

// 메소드
List<Order> findByAddressCityOrOrderAmount(String city, int orderAmount);

// 사용
List<Order> or = orderRepository.findByAddressCityOrOrderAmount("cityA", 5);

 

(2) Is, Equals

Is나 Equals를 메소드명 마지막에 붙이게 된다면 파라미터 값에 대한 equal 메소드를 생성해준다

이를 생략하면 기본적으로 equal 메소드를 생성해준다

// 메소드
List<Member> findByAge(int age);
List<Member> findByAgeIs(int age);
List<Member> findByAgeEquals(int age);

// 사용
List<Member> byAge = memberRepository.findByAge(25);
List<Member> byAgeIs = memberRepository.findByAgeIs(25);
List<Member> byAgeEquals = memberRepository.findByAgeEquals(25);

 

(3) Between

[첫번째 파라미터 ~ 두번째 파라미터]의 범위에 속한 값들을 구하는 쿼리가 생성된다

// 메소드
List<Member> findByAgeBetween(int age1, int age2);
List<Order> findByOrderDateBetween(LocalDateTime first, LocalDateTime second);

// 사용
List<Member> byAgeBetween = memberRepository.findByAgeBetween(15, 20);
List<Order> byOrderDateBetween = orderRepository.findByOrderDateBetween(
        LocalDateTime.of(2022, 8, 1, 0, 0, 0),
        LocalDateTime.of(2022, 8, 5, 0, 0, 0)
);

 

(4) LessThan["<"], LessThanEqual["≤"]

// 메소드
List<Member> findByAgeLessThan(int age);
List<Member> findByAgeLessThanEqual(int age);

// 사용
List<Member> byAgeLessThan = memberRepository.findByAgeLessThan(20);
List<Member> byAgeLessThanEqual = memberRepository.findByAgeLessThanEqual(20);

 

(5) GreaterThan[">"], GreaterThanEqual["≥"]

// 메소드
List<Member> findByAgeGreaterThan(int age);
List<Member> findByAgeGreaterThanEqual(int age);

// 사용
List<Member> byAgeLessThan = memberRepository.findByAgeGreaterThan(20);
List<Member> byAgeLessThanEqual = memberRepository.findByAgeGreaterThanEqual(20);

 

(6) Before["<"], After[">"]

사실 Before/After 키워드는 LessThan/GreaterThan 키워드랑 기능은 동일하지만, 키워드 이름만으로 봤을때는 "날짜"와 관련된 쿼리를 생성할 때 사용하는 것이 더 바람직하다

// 메소드
List<Member> findByAgeBefore(int age);
List<Member> findByAgeAfter(int age);
List<Order> findByOrderDateBefore(LocalDateTime before);
List<Order> findByOrderDateAfter(LocalDateTime after);

// 사용
List<Member> byAgeLessThan = memberRepository.findByAgeBefore(20);
List<Member> byAgeLessThanEqual = memberRepository.findByAgeAfter(20);

List<Order> byOrderDateBefore = orderRepository.findByOrderDateBefore(
        LocalDateTime.of(2022, 8, 2, 0, 0, 0)
);
List<Order> byOrderDateAfter = orderRepository.findByOrderDateAfter(
        LocalDateTime.of(2022, 8, 2, 0, 0, 0)
);

 

(7) IsNull, IsNotNull/NotNull

// 메소드
List<Member> findByTeamIsNull();
List<Member> findByTeamIsNotNull();
List<Member> findByTeamNotNull();

// 사용
List<Member> byTeamIsNull = memberRepository.findByTeamIdIsNull();
List<Member> byTeamIsNotNull = memberRepository.findByTeamIsNotNull();
List<Member> byTeamNotNull = memberRepository.findByTeamNotNull();

 

List<Member> findByTeamIsNull();
List<Member> findByTeamIdIsNull();

이 두 쿼리 메소드의 차이점은 다음과 같다

findBy"Team"IsNullMember 엔티티의 Team이 null인지 확인하는 쿼리 메소드이다

그에 반면에 findBy"TeamId"IsNullMember 엔티티와 연관된 "Team엔티티 의 Id"가 null인지 확인하는 쿼리 메소드이다

A라는 엔티티의 레포지토리에서 B라는 엔티티에 대한 검증을 하기 위해서는 B 엔티티에 대한 필드 조건을 명시해주면 된다

 

(8) Like, NotLike

// 메소드
List<Member> findByUsernameLike(String like);
List<Member> findByUsernameNotLike(String notLike);

// 사용
List<Member> byUsernameLike = memberRepository.findByUsernameLike("%nus1%");
List<Member> byUsernameNotLike = memberRepository.findByUsernameNotLike("%nus1%");

 

(9) StartingWith[~~%], EndingWith[%~~], Containing[%~~%]

// 메소드
List<Member> findByUsernameStartingWith(String start);
List<Member> findByUsernameEndingWith(String end);
List<Member> findByUsernameContaining(String contain);

// 사용
List<Member> byUsernameStartingWith = memberRepository.findByUsernameStartingWith("Avenus1");
List<Member> byUsernameEndingWith = memberRepository.findByUsernameEndingWith("1");
List<Member> byUsernameContaining = memberRepository.findByUsernameContaining("nus1");

 

(10) OrderBy

[필드 이름]OrderBy[Order 기준]뒤에 DESC를 붙이면 내림차순 정렬, ASC를 붙이면 오름차순 정렬로 쿼리가 생성된다

// 메소드
List<Member> findByUsernameContainingOrderByAgeDesc(String username);
List<Member> findByUsernameContainingOrderByAgeAsc(String username);

// 사용
List<Member> byUsernameContainingOrderByAgeDesc = memberRepository.findByUsernameContainingOrderByAgeDesc("1");
List<Member> byUsernameContainingOrderByAgeAsc = memberRepository.findByUsernameContainingOrderByAgeAsc("1");

 

(11) Not

파라미터로 들어간 값이 "아닌" 결과들을 찾는 쿼리를 생성한다

// 메소드
List<Member> findByAgeNot(int age);

// 사용
List<Member> byAgeNot = memberRepository.findByAgeNot(25);

 

(12) In, NotIn

In이나 NotIn을 사용한 쿼리 메소드는 파라미터로 "In이나 NotIn에 대한 검증을 할 수 있는 컬렉션"을 넘겨줘야 한다

// 메소드
List<Member> findByAgeIn(Collection<Integer> age);
List<Member> findByAgeNotIn(Collection<Integer> age);

// 사용
List<Integer> list = new ArrayList<>(){
    {
        add(25);
        add(30);
        add(16);
    }
};

List<Member> byAgeIn = memberRepository.findByAgeIn(list);
List<Member> byAgeNotIn = memberRepository.findByAgeNotIn(list);

파라미터로 넘어가는 컬렉션의 타입은 "java.util.Collection"이다

 

(13) TRUE, FALSE

// 메소드
List<Member> findByEnabledTrue();
List<Member> findByEnabledFalse();

// 사용
List<Member> byIsAdminTrue = memberRepository.findByEnabledTrue();
List<Member> byIsAdminFalse = memberRepository.findByEnabledFalse();

DB Query상으로 [TRUE = 1, FALSE = 0]으로 치환된다

 

(14) Limit → First[페이징], Top[페이징]

find + [....] + By에서 ...부분에 First를 붙이게 되면 "첫번째 Result"만 반환받게 된다.

Top[숫자]를 붙이게 된다면 "숫자"만큼의 페이징 데이터를 반환받게 된다

// 메소드
Optional<Member> findFirstByUsernameContainingOrderByAgeAsc(String contain);
Optional<Member> findTopByUsernameContainingOrderByAgeAsc(String contain);
List<Member> findTop2ByUsernameContainingOrderByAgeAsc(String contain);
List<Member> findTop10ByUsernameContainingOrderByAgeAsc(String contain);

// 사용
Optional<Member> firstByUsernameContaining = memberRepository.findFirstByUsernameContainingOrderByAgeAsc("nus1");
Optional<Member> topByUsernameContaining = memberRepository.findTopByUsernameContainingOrderByAgeAsc("nus1");
List<Member> top2ByUsernameContaining = memberRepository.findTop2ByUsernameContainingOrderByAgeAsc("nus1");
List<Member> top10ByUsernameContaining = memberRepository.findTop10ByUsernameContainingOrderByAgeAsc("nus1");

왼쪽 = findFirstBy... / 오른쪽 = findTopBy...
왼쪽 = findTop2By... / 오른쪽 = findTop10By...

 

(15) count...By

count..By 쿼리 메소드는 "결과의 Row Count"를 반환하는 쿼리 메소드이고 Return Type은 "반드시 Long"이여야 한다

// 메소드
Long countBy();
Long countAllBy();
Long countByAgeGreaterThanEqual(int age);
Long countByAgeGreaterThanEqualAndUsernameContaining(int age, String contain);

// 사용
Long countBy1 = memberRepository.countBy();
Long countBy2 = memberRepository.countAllBy();
Long countBy3 = memberRepository.countByAgeGreaterThanEqual(25);
Long countBy4 = memberRepository.countByAgeGreaterThanEqualAndUsernameContaining(25, "nus1");

왼쪽 = countBy / 오른쪽 = countAllBy
왼쪽 = countByAge... / 오른쪽 = countByAge...

 

(16) delete...By, remove...By

delete/remove...By는 조건에 대해서 Table에서 Instance를 지우는 쿼리 메소드이다.

Return Type은 "Table에서 Modify된 Row의 수 = Long"이다

// 메소드
Long deleteBy();
Long removeBy();

// 사용
Long deleteCount = orderRepository.deleteBy();

 

// 메소드
Long deleteByOrderAmountGreaterThan(int orderAmount);

// 사용
Long count = orderRepository.deleteByOrderAmountGreaterThanEqual(75);

 

// 메소드
Long deleteByOrderAmountLessThanEqualAndOrderDateBefore(int orderAmount, LocalDateTime orderDate);

// 사용
Long count = orderRepository.deleteByOrderAmountLessThanEqualAndOrderDateBefore(
        100,
        LocalDateTime.of(2022, 7, 31, 0, 0, 0)
);

 

(17) exists...By

exists...By는 조건을 통해서 해당 조건을 만족하는 Instance가 하나라도 있는지 판별하는 쿼리 메소드이다

Return Type은 당연히 boolean이고, 만약 조건이 없다면 Table에 Instance가 있기만 하면 true를 return하게 된다

// 메소드
boolean existsBy();

// 사용
boolean isExists = orderRepository.existsBy();

exists..By는 실질적으로 "PK 기준 Limit 1"조건을 통해서 하나라도 있는지 select query를 통해서 판별한다

 

// 응용 : 모두 삭제 후 existsBy 호출
orderRepository.deleteBy();
boolean isExists = orderRepository.existsBy();

모두 삭제를 하고 "existsBy"를 호출하였더니 당연히 exists query는 "select를 통해서 ID값이 존재하는지 확인"하고, 전부 삭제했으므로 당연히 Instance가 없고 그에 따라서 false를 return하게 된다

 

// 메소드
boolean existsByOrderAmountGreaterThanEqual(int orderAmount);

// 사용
boolean isExists = orderRepository.existsByOrderAmountGreaterThanEqual(100);
boolean isExists = orderRepository.existsByOrderAmountGreaterThanEqual(101);

 

 

2. 메소드 이름으로 JPA NamedQuery 호출

Entity Level에 @NamedQueries/@NamedQuery를 통해서 엔티티만의 쿼리를 미리 정의해놓고, 인터페이스 레포지토리에서 불러와서 사용할 수 있다

@NamedQueries({
        @NamedQuery(
                name = "Member.findByUsernameNamedQuery",
                query = "SELECT m FROM Member m where m.username = :username"
        ),
        @NamedQuery(
                name = "Member.findByAgeGreaterThanEqualNamedQuery",
                query = "SELECT m FROM Member m where m.age >= :age"
        )
})
public class Member {
    ...
    ...
}
List<Member> findByUsernameNamedQuery(String username);
List<Member> findByAgeGreaterThanEqualNamedQuery(int age);

이렇게 Entity에 NamedQuery를 미리 정의해놓고 인터페이스 레포지토리에 선언함으로써 사용할 수 있다

 

 

3. @Query 애노테이션을 통해서 레포지토리 인터페이스에 직접 JPQL 정의

인터페이스 레포지토리에 "직접" JPQL이나 native query를 정의하기 위해서는 @Query 내부에 정의해주면 된다

▶ value

value는 의미 그대로 "정의할 JPQL"을 작성해주면 된다

List<Member> findBy();

@Query(value = "SELECT m" +
                " FROM Member m" +
                " JOIN FETCH m.team")
List<Member> findByAnnotationQuery();

왼쪽 = findBy / 오른쪽 = findByAnnotationQuery

단순히 쿼리 메소드 기능만으로 쿼리를 날리면 "연관된 엔티티에 대한 fetch join"을 날리지 못한다

fetch join이 필요한 상황에서만 반드시 @Query를 통해서 JPQL을 직접 정의해줘야 한다

 

▶ countQuery

countQuery는 "Paging"과정에서 Page에 존재하는 Element의 개수를 파악할 때 필요한 쿼리이다

@Query(value = "SELECT m" +
                " FROM Member m" +
                " JOIN FETCH m.team",
        countQuery = "SELECT count(m) FROM Member m")
Page<Member> findByAnnotationPagingQuery(Pageable pageable);

페이징 쿼리
페이징에 대한 count query

 

▶ countProjection

countProjection이란 count query를 날릴 때 "count의 기준"이 되는 필드 및 엔티티를 선택하는 것이다

@Query(value = "SELECT m" +
        " FROM Member m",
        countProjection = "m")
Page<Member> findByAnnotationPagingQueryCountProjection0(Pageable pageable);

@Query(value = "SELECT m" +
                " FROM Member m",
        countProjection = "m.id")
Page<Member> findByAnnotationPagingQueryCountProjection1(Pageable pageable);

@Query(value = "SELECT m" +
        " FROM Member m",
        countProjection = "m.age")
Page<Member> findByAnnotationPagingQueryCountProjection2(Pageable pageable);

1 = m / 2 = m.id / 3 = m.age

기본적으로 엔티티 자체를 countProjection으로 설정하게 되면 "엔티티의 PK"가 count query의 기준이 되어서 해당 쿼리가 날라가게 된다

 

▶ nativeQuery

"nativeQuery = true"로 설정하게 된다면 JPQL이 아니라 순수한 SQL 자체를 작성할 수 있다

@Query(value = "SELECT *" +
                " FROM Member m" +
                " WHERE m.age > 25",
        nativeQuery = true)
List<Member> findByAnnotationNativeQuery();

 

▶ name

name은 "Entity에 정의한 @NamedQuery"를 직접 @Query 내부에 정의해서 사용하는 것이다

@NamedQueries({
        @NamedQuery(
                name = "Member.findByUsernameNamedQuery",
                query = "SELECT m FROM Member m where m.username LIKE :username"
        ),
        @NamedQuery(
                name = "Member.findByAgeGreaterThanEqualNamedQuery",
                query = "SELECT m FROM Member m where m.age >= :age"
        )
})
public class Member {
    ....
    ....
}
@Query(name = "Member.findByUsernameNamedQuery")
List<Member> findByNamedQuery1(@Param("username") String username);

@Query(name = "Member.findByAgeGreaterThanEqualNamedQuery")
List<Member> findByNamedQuery2(@Param("age") int age);

왼쪽 = Member.finByUserNamedQuery / 오른쪽 = findByAgeGreaterThanEqualNameQuery

 

▶ countName

countName은 @NamedQuery에 정의된 "count query"를 직접 @Query 내부에 지정해서 사용하는 것이다

countName"Paging에 대한 countQuery"를 미리 @NamedQuery로 작성하고 설정하는 것이다

@NamedQueries({
        @NamedQuery(
                name = "Member.findByUsernameNamedQuery",
                query = "SELECT m FROM Member m where m.username LIKE :username"
        ),
        @NamedQuery(
                name = "Member.findByAgeGreaterThanEqualNamedQuery",
                query = "SELECT m FROM Member m where m.age >= :age"
        ),
        @NamedQuery(
                name = "Member.count",
                query = "SELECT count(m) FROM Member m"
        )
})
public class Member {
    ....
    ....
}
// 메소드
@Query(value = "SELECT m" +
        " FROM Member m",
        countName = "Member.count")
Page<Member> findByAnnotationPagingQueryCountProjectionCountName1(Pageable pageable);

@Query(value = "SELECT m" +
        " FROM Member m",
        countName = "Member.count")
Page<Member> findByAnnotationPagingQueryCountProjectionCountName2(Pageable pageable);

// 사용
memberRepository.findByAnnotationPagingQueryCountProjectionCountName1(PageRequest.of(0, 20));
memberRepository.findByAnnotationPagingQueryCountProjectionCountName2(PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "username")));

 

4. 벌크성 수정 쿼리

Spring Data JPA에서 인터페이스 레포지토리에 벌크성 수정 쿼리를 적용하고 싶다면 @Query 내부에 JPQL or nativeQuery를 작성하고 "@Modifying"을 붙여줘야 한다

@Modifying을 붙인 메소드의 return type은 [void, int, Integer]중에 하나로 선택해야 한다
@Query("UPDATE Order o" +
        " SET o.orderAmount = 100" +
        " WHERE o.orderAmount <= 20")
@Modifying
void updateByBulk();

@Query("DELETE FROM Order o" +
        " WHERE o.orderDate >= '2022-07-31 00:00:00'")
@Modifying
Integer deleteByBulk();

벌크성 쿼리의 특징은 영속성 컨텍스트를 거치지 않고 DB로 Direct로 Query로 날라가는 것이다
따라서 벌크성 쿼리를 날리고 나서 웬만하면 영속성 컨텍스트를 clear해주는 것이 좋은 방법이다
>> @Modifying(clearAutomatically = true)로 옵션을 키게 되면 벌크성 쿼리를 날리고나서 영속성 컨텍스트를 clear해준다

 

※ 파라미터 바인딩 (위치 vs 이름)

기본적으로 쿼리 메소드의 파라미터 바인딩 기준은 "위치"이다

@Query("SELECT m FROM Member m WHERE m.username LIKE ?1 AND m.age >= ?2")
List<Member> findByParameterBindingPosition1(String username, int age);

@Query("SELECT m FROM Member m WHERE m.username LIKE ?1 AND m.age >= ?2")
List<Member> findByParameterBindingPosition2(int age, String username);

 

1) memberRepository.findByParameterBindingPosition1("nus1", 25);
2) memberRepository.findByParameterBindingPosition2(25, "nus1");

과연 이 두 메소드는 모두 정상적으로 실행이 되는 걸까??

두번째 호출 메소드는 당연히 오류가 발생한다

왜냐하면 쿼리 메소드를 보면 [첫번째 파라미터 = username, 두번째 파라미터 = age]로 들어와야 하는데 실제 호출되는 값을 보면 반대로 들어갔기 때문이다

>> 이렇게 "위치"기준 파라미터 바인딩을 사용하면 개발자가 혼동하기 쉽다

JPQL에서 위치 기준 파라미터 바인딩을 활용하면 위치 기준 첫번째는 "1"이다
반면에 nativeQuery에 대해서 위치 기준 파라미터 바인딩을 활용하면 위치 기준 첫번째는 "0"이다

 

따라서 그냥 무조건 "이름"기준 파라미터 바인딩을 활용하자

@Query("SELECT m FROM Member m WHERE m.username LIKE :username AND m.age >= :age")
List<Member> findByParameterBindingName1(@Param("username") String username, @Param("age") int age);

@Query("SELECT m FROM Member m WHERE m.username LIKE :username AND m.age >= :age")
List<Member> findByParameterBindingName2(@Param("age") int age, @Param("username") String username);
memberRepository.findByParameterBindingName1("nus1", 25);
memberRepository.findByParameterBindingName2(25, "nus1");

위치 기준 파라미터와 동일하게 호출해보자

명확한 "이름"을 기준으로 파라미터들을 바인딩하기 때문에 위치와 다르게 혼동될 일이 전혀 없이 정확하게 파라미터가 바인딩 됨을 확인할 수 있다