궁금한점이 여러개 생겼습니다.
18
投稿した質問数 12
안녕하세요, 2회독 하는중입니다. 확실히 강의 활용하면서 진행하니깐 느끼기(?)가 뭔지 알것같고 궁금한점이 생기네요. CouponService 에서 이미 다운받은 쿠폰은 내려주지 않는 로직을 만들면서 궁금한점이 여러개 생겼습니다.
작성하면서 정리하니깐 궁금한점이 계속 꼬리물어서 생긴다고 해야하나 그래서 의식의 흐름대로 적는 것 같아서 미리 죄송합니다 🥲
/// CouponService
public List<Coupon> getCouponsForMenus(Principal principal, Collection<Long> couponIds) {
Set<Long> applicableCouponIds = couponFinder.findApplicableCouponIds(couponIds);
Set<Long> downloadedCouponIds = issuedCouponFinder.findDownloadedCouponIds(principal.getId());
Set<Long> downloadableCouponIds = applicableCouponIds.stream()
.filter(id -> !downloadedCouponIds.contains(id))
.collect(Collectors.toSet());
return couponFinder.findAllById(downloadableCouponIds);
}
// CouponFinder
public Set<Long> findApplicableCouponIds(Collection<Long> menuIds) {
List<CouponTargetEntity> menuTargets = couponTargetRepository.findByTargetTypeAndTargetIdIn(
CouponTargetType.MENU,
menuIds
).stream().filter(CouponTargetEntity::isActive).toList();
List<CouponTargetEntity> categoryTargets = couponTargetRepository.findByTargetTypeAndTargetIdIn(
CouponTargetType.MENU_CATEGORY,
menuCategoryRepository.findByCategoryIdIn(menuIds).stream()
.filter(MenuCategoryEntity::isActive)
.map(MenuCategoryEntity::getCategoryId).collect(Collectors.toSet())
);
return Stream.concat(menuTargets.stream(), categoryTargets.stream())
.map(CouponTargetEntity::getCouponId)
.collect(Collectors.toSet());
}
CouponService 에서 implement 계층 CouponFinder, IssuedCouponFinder 를 의존하고 있는 상황에서 각 계층끼리 소통할 때
강사님 Q/A 답변중에서 가능한 db 엔티티 대신에 개념객체를 넘기는 방향으로 설계하는걸 지향하신다고 말씀하신 답변이 기억이 나는데요, 지금 상황에서는 개념객체까지 변환하는것보다는 id 컬렉션만 뽑아내서 넘기는게 더 효율적이고 깔끔한 것 같습니다.
이런 패턴(?) 이 자주 나올 것 같아서 괜찮은지 궁금 합니다.
꼭 id 컬렉션을 넘기는게 아니더라도 entity를 반환해야만 매핑하기 편한 경우가 많을 것 같더라구요
회원/비회원 요구사항
해당 Q/A 를 보기도 했고 마침 강의 활용해서 커피 판매 어플리케이션을 만들고 싶어서 비회원처리가 필요했습니다.
public class PrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private static final String PRINCIPAL_ID_HEADER = "Gu-Coffee-Principal-Id";
private static final String PRINCIPAL_TYPE_HEADER = "Gu-Coffee-Principal-Type";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Principal.class);
}
@Override
public @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) throw new CoreException(ErrorType.INVALID_REQUEST, null);
Authenticated annotation = parameter.getParameterAnnotation(Authenticated.class);
String id = request.getHeader(PRINCIPAL_ID_HEADER);
String type = request.getHeader(PRINCIPAL_TYPE_HEADER);
validatePrincipal(annotation, id, type);
return Principal.of(id, type);
}
private void validatePrincipal(Authenticated annotation, String id, String type) {
boolean isRequired = (annotation != null && annotation.required());
if (isRequired) {
if (id == null) throw new CoreException(ErrorType.UNAUTHORIZED, null);
if (!PrincipalType.USER.name().equals(type)) {
throw new CoreException(ErrorType.UNAUTHORIZED, "회원 전용 서비스입니다.");
}
}
if (type == null) {
throw new CoreException(ErrorType.INVALID_REQUEST, null);
}
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated {
boolean required() default true;
}
저는 Authenticated 라는 메타 어노테이션을 사용해서
@GetMapping("/v1/issued-coupons")
private ApiResponse<List<IssuedCouponResponse>> getIssuedCoupons(@Authenticated Principal principal) {
List<IssuedCoupon> issuedCoupons = issuedCouponService.getIssuedCoupons(principal);
return ApiResponse.success(IssuedCouponResponse.from(issuedCoupons));
}
@PostMapping("/v1/coupons/{couponId}/download")
private ApiResponse<?> download(
Principal principal,
@PathVariable Long couponId) {
couponService.download(principal, couponId);
return ApiResponse.success();
}
public void download(Principal principal, Long couponId) {
if (principal.getType().equals(PrincipalType.GUEST)) {
throw new CoreException(ErrorType.UNAUTHORIZED, "비회원은 쿠폰을 다운로드할 수 없습니다.");
}
애초에 회원만 접근 가능한 api 는 어노테이션으로 처리하던가 서비스 단에서 예외를 던지게 할 수도 있을 것 같아서 이렇게 만들어 봤습니다.
이런식의 접근이 괜찮은지
당장은 인증객체(Principal)? 랑 User 랑은 다른 개념일 것 같아서 강의내 User 대신에 사용했습니다.
@Table(name = "cart_item")
@Entity
public class CartItemEntity extends BaseEntity {
private Long userId;
private String guestKey;
@Enumerated(EnumType.STRING)
private PrincipalType principalType;
private Long menuId;
private Long quantity;
}
public class Cart {
private Principal principal;
private List<CartItem> items;
}
마지막으로 장바구니에 비회원도 담을 수 있다는 요구사항이 있다고 가정하면 테이블에는 userId 만 둬서 null 로 구분하는 것 보다 key 필드를 추가하고
public interface CartItemRepository extends JpaRepository<CartItemEntity, Long> {
@Query("SELECT c FROM CartItemEntity c " +
"WHERE (:type = 'USER' AND c.userId = :id) " +
"OR (:type = 'GUEST' AND c.guestKey = :key)")
List<CartItemEntity> findByPrincipal(
@Param("type") String type,
@Param("id") Long id,
@Param("key") String key
);
}
이런식으로 쿼리로 분기처리해야지 생각했는데요. 예전 강사님 Q/A 답변 중에서 쿼리중심의 어플리케이션이 되는걸 지양한다는 느낌의 답변을 봤던 것 같습니다.
AI 한테 물어봤을때는 해당 로직은 필터링 목적이니 쿼리로 처리하는게 성능상 효과적이라는 답변을 줬는데요.
List<IssuedCouponEntity> issuedCoupons = issuedCouponRepository.findByUserId(userId)
.stream()
.filter(IssuedCouponEntity::isActive)
.toList();
isActive 인 데이터를 가져올 때 강사님이 findByXXAndStatus 로 한번에 가져오거나 혹은 위처럼 filter로 처리하는 두가지 패턴을 보여주셨습니다.
이걸 처음 봤을 때, 미세한 성능보다는 filter 로 직관적으로 활성상태인 데이터만 가져온다는 목적을 코드로 보여주는게 더 괜찮겠다고 생각했는데요.
근데 이것도 결국 필터링 목적이니깐 메서드 네이밍(findByXXAndStatus)으로 처리하는게 일관성 있는게 아닌지 궁금합니다.
回答 0
다양한 관점의 코드 경험을 위해 개선하지 않은 코드
1
51
1
histories() 응답에 PointHistory.id를 포함한 이유가 궁금합니다/
1
44
2
SettlementTargetRepository Jquery 질문
1
48
2
부가 기능을 이벤트 핸들러로 분리하는 기준이 있을까요?
1
60
2
엔티티의 pk 를 0으로 초기화하시는 이유가 있을까요??
1
67
2
제미니님 안녕하세요!
1
74
2
개념 간 격벽 분리와 목록 조회 시 발생하는 참조 구조
1
82
2
프로덕트와 프로덕트카테고리 사이의 삭제 정책
1
75
2
새로 개발한다면 구현 순서
1
134
1
의존 방향에 대한 고민
1
124
2
어드민(Back-office)에서 예약 변경 시, '할인 조건 재검증(쿠폰 회수)' vs '기존 혜택 유지' 중 어떤 정책이 일반적인가요?
1
96
2
OrderKeyGenerator 인스턴스화 generate() 질문
1
83
1
외부 API 통합 시 데이터 제어 범위 설계 질문
1
96
1
PG 결제 승인 로직
1
129
2
QnA에서 Join 필드 표현법
1
89
1
결제서비스 콜백 동시성문제 가능성
1
107
2
굿
1
108
1
도메인/엔티티 분리 상황에서 쓰기 작업 하는 방법
1
135
2
도메인 객체와 엔티티 객체 사용
1
138
2
CouponService 의존성 의문
1
97
2
상품 목록 조회 고도화 질문
1
111
2
표현 계층에서의 접근 지점이 다양해지는것과 이를 해결하기 위한 파사드의 도입에 대해 제미니님의 생각이 궁금합니다.
1
123
2
제품상세 코드 느끼기
1
144
2
격벽의 순환 참조(?)
1
113
2

