※ 어드민 기능: Member 조회
1. 기간 설정 (생일, 가입일)
2. 필터링 조건 설정 (MemberId, 이메일, 닉네임, 생년월일, 성별, 권한, 임신 유무, 흡연 유무, 고혈압 유무, 당뇨병 유무)
→ Member 객체 (MemberId, 이메일, 닉네임, 생년월일, 성별, 권한), Profile 객체 (임신 유무, 흡연 유무, 고혈압 유무, 당뇨병 유무)
→ Profile은 메인 프로필만 사용한다. (추가 프로필은 사용 X)
→ 이메일, 닉네임은 대소문자를 구분하지 않는다.
조건이 두가지만 들어가서 필터링 자체는 간단하다. QueryDsl보다는 JPA Specification이 더 적합하다.
JPA Specification
JPA Specification은 JPA의 Criteria API를 기반으로 하며, 동적 쿼리 생성을 위한 표준 JPA API입니다.
주요 특징:
- 표준화: JPA Specification은 JPA의 표준 API이므로, 모든 JPA 구현체에서 호환됩니다.
- 동적 쿼리: Specification 객체를 사용하여 동적으로 쿼리를 조합할 수 있습니다.
- 유연성: 쿼리 조건을 메서드 체이닝으로 조합할 수 있으며, 복잡한 쿼리 조건을 표현할 수 있습니다.
- 설정 간편성: JPA Repository와 통합되어 있어, JpaSpecificationExecutor를 통해 쉽게 사용할 수 있습니다.
적합한 상황:
- 표준 JPA 환경: JPA를 사용하는 프로젝트에서 JPA의 표준 API를 사용하여 동적 쿼리를 생성하고 싶을 때.
- 간단한 쿼리: 복잡하지 않은 동적 쿼리를 작성할 때.
- JPA에 익숙한 개발자: JPA의 기존 메커니즘에 익숙하고 추가적인 의존성을 피하고 싶을 때.
[module-common] → Member default-profile 컬럼 추가
: 필터링시 메인(default) 프로필만 사용하는데, 프로필 DB의 owner값이 true를 찾아 처리하니 복잡해져서 default-profile에 대한 컬럼을 추가했음.
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
@Column(unique = true)
private String nickname;
@Column(unique = true)
private String phone;
@Enumerated(EnumType.STRING)
private Gender gender;
@Column
private String password;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate birthday;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Column(name = "created_at")
private LocalDate createdAt;
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
private Provider provider;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "default_profile_id")
private Profile defaultProfile;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Profile> profiles = new ArrayList<Profile>();
.
.
}
[module-admin] → AdminController
: 필터링 조건
- filed: id, email, nickname, gender, role
- value: filed 값
- profileFiled: pregnancy, smoking, hypertension, diabetes
- profileValue: YES, NO, UNKNOWN
- dateFiled: birthday, createdAt
- startDate, endDate
@GetMapping("/search")
public ResponseEntity<List<FilterResponse>> searchMembers(
@RequestParam(required = false) String field,
@RequestParam(required = false) Object value,
@RequestParam(required = false) String profileField,
@RequestParam(required = false) Object profileValue,
@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate,
@RequestParam(required = false) String dateField
) {
List<FilterResponse> responses = adminService.searchMembers(field, value, profileField, profileValue, startDate, endDate, dateField);
return ResponseEntity.ok(responses);
}
[module-admin] → AdminServiceImpl
spec.and를 사용하여 여러 조건 결합
public List<FilterResponse> searchMembers(String field, Object value, String profileField, Object profileValue, LocalDate startDate, LocalDate endDate, String dateField) {
Specification<Member> spec = Specification.where(null);
if (value != null) {
spec = spec.and(MemberSpecification.filterByField(field, value));
}
if (profileValue != null) {
spec = spec.and(MemberSpecification.filterByProfileField(profileField, profileValue));
}
if (startDate != null && endDate != null && dateField != null) {
spec = spec.and(MemberSpecification.filterByDateRange(dateField, startDate, endDate));
}
List<Member> members = memberRepository.findAll(spec);
return members.stream()
.map(MemberMapper::toFilterResponse)
.collect(Collectors.toList());
}
[module-admin] → MemberSpecification
<filterByField>
: field에 해당하는 value를 가지고 일치하는 데이터 반환. 이메일과 닉네임은 해당 value가 포함될 경우 반환
Like VS Contains
1) 간단한 패턴 매칭: LIKE는 간단한 문자열 패턴 매칭에는 적합하지만, 인덱스의 활용 여부에 따라 성능이 저하될 수 있습니다.
2) 전문 검색: CONTAINS는 대량의 텍스트 데이터를 처리하는 데 최적화되어 있으며, Full-Text Search 인덱스를 활용하여 검색 성능을 높입니다. 대량의 데이터를 처리할 때 CONTAINS가 더 나은 성능을 보일 수 있습니다.
JPA Specification에서는 contains를 사용할 수 없고 like만 사용가능하다. 만약 더 좋은 성능이 필요하다면 이후에 QueryDsl로 변경하도록 하자
public static Specification<Member> filterByField(String field, Object value) {
return (root, query, criteriaBuilder) -> {
if (field != null && value != null) {
switch (field) {
case "id":
return criteriaBuilder.equal(root.get("id"), value);
case "email":
// 대소문자를 무시하고 포함 여부 검사
String lowerEmail = ((String) value).toLowerCase();
return criteriaBuilder.like(
criteriaBuilder.lower(root.get("email")),
"%" + lowerEmail + "%"
);
case "nickname":
// 대소문자를 무시하고 포함 여부 검사
String lowerNickname = ((String) value).toLowerCase();
return criteriaBuilder.like(
criteriaBuilder.lower(root.get("nickname")),
"%" + lowerNickname + "%"
);
case "gender":
return criteriaBuilder.equal(root.get("gender"), value);
case "role":
return criteriaBuilder.equal(root.get("role"), value);
}
}
return null; // 아무 필터도 없을 경우 null 반환
};
}
<filterByProfileField>
: root.join 기능을 사용하여 Member와 Profile을 조인하여 필터링을 수행
public static Specification<Member> filterByProfileField(String profileField, Object profileValue) {
return (root, query, criteriaBuilder) -> {
if (profileField != null && profileValue != null) {
Join<Member, Profile> profile = root.join("defaultProfile");
switch (profileField) {
case "pregnancy":
return criteriaBuilder.equal(profile.get("pregnancy"), profileValue);
case "smoking":
return criteriaBuilder.equal(profile.get("smoking"), profileValue);
case "hypertension":
return criteriaBuilder.equal(profile.get("hypertension"), profileValue);
case "diabetes":
return criteriaBuilder.equal(profile.get("diabetes"), profileValue);
}
}
return null;
};
}
<filterByDateRange>
public static Specification<Member> filterByDateRange(String dateField, LocalDate startDate, LocalDate endDate) {
return (root, query, criteriaBuilder) -> {
if (startDate != null && endDate != null && dateField != null) {
switch (dateField) {
case "birthday":
return criteriaBuilder.between(root.get("birthday"), startDate, endDate);
case "createdAt":
return criteriaBuilder.between(root.get("createdAt"), startDate, endDate);
}
}
return null;
};
}
Postman 테스트 성공
'개발' 카테고리의 다른 글
[스프링부트] 마케팅 수신 동의 멤버 조회, @Cache- (0) | 2024.10.02 |
---|---|
[스프링부트] 필수 약관 - Native Query (0) | 2024.09.25 |
[스프링부트] LazyInitializationException 지연 로딩 오류 (0) | 2024.09.12 |
[스프링부트] 탈퇴 회원 처리 (Postgres DB 분리, @Scheduled) (0) | 2024.09.10 |
[Spring Cloud Gateway] Admin 로그인 API가 따로 필요한가 ? ? (0) | 2024.09.10 |