[Spring Boot] Spring에서 프록시 패턴을 이용해 복잡한 기능 구현해보기
들어가며
프록시 패턴 (Proxy Pattern)은 객체 지향 디자인 패턴 중 하나로, 객체 간의 간접적인 접근을 가능하게 하는 구조를 제공하는 패턴이다. 이번 게시글에선 프록시 패턴을 이용하여 전혀 다른 2가지 이상의 요구사항을 구현한 경험에 대해 정리해보려고 한다.
프록시 패턴에 대한 설명은 해당 글을 참고하면 된다.
링크 : https://jangjjolkit.tistory.com/59
여기 클라이언트의 요구사항이 있다.
- 사용자가 리뷰를 진행중인 이벤트 쿠폰을 지급하고, 마찬가지로 진행중인 이벤트 포인트를 지급한다.
- 리뷰는 A, B, C, D 라고하는 서로 다른 동작을 진행 후에 작성할 수 있다.
- 리뷰 작성 시 각 동작에 따라 리뷰 작성이 가능한지 유효성 체크 기능를 진행해야 한다.
- 쿠폰, 포인트 지급은 동작에 관계없이 리뷰를 작성하면 모두 지급한다.
리뷰는 A, B, C, D 동작 이후에 작성할 수 있다. 리뷰 작성 가능 유효성 체크는 각 동작에 따라 기능이 완전히 다르다. 하지만 최종적으로 리뷰 작성이라는 기능은 동일하고, 마찬가지로 쿠폰 및 포인트 지급이라는 기능 역시 동일하다.
1. 리뷰 작성을 제외한 요구사항 Service 구현
먼저 리뷰 작성 기능을 제외한 나머지 기능들을 각 Service별로 구현한다. 리뷰 작성은 나머지 기능을 먼저 만든 후에 구현하려고 한다.
쿠폰 지급
// coupon service
public interface CouponService {
// 쿠폰 지급
void issueCoupon() throws Exception;
}
// coupon service 구현
@Service
@RequiredArgsConstructor
public class CouponServiceImpl implements CouponService {
// coupon issue mapper
private final CouponMapper couponMapper;
@Override
@Transactional
public void issueCoupon() throws Exception {
// ... 쿠폰 지급 기능
}
}
포인트 지급
// point service
public interface PointService {
// 포인트 지급
void issuePoint() throws Exception;
}
// point service 구현
@Service
@RequiredArgsConstructor
public class PointServiceImpl implements PointService {
// point issue mapper
private final PointMapper pointMapper;
@Override
@Transactional
public void issuePoint() throws Exception {
// ... 포인트 지급 기능
}
}
2. 리뷰 작성 기능 구현
다음으로, 각 동작별 리뷰 작성 기능을 구현한다. 리뷰 작성이라는 공통적인 기능이 동작별로 다르게 동작하기 때문에, 리뷰 작성이라는 하나의 Interface를 만들고, 동작별로 구현한다.
동작별 리뷰 작성 Interface 구현체에 @Qualifier 어노테이션 적용한다. 이후 프록시 객체에 @Primary 어노테이션을 적용할 예정이다.
리뷰 동작 Enum
public enum ReviewKind {
A, B, C, D;
}
리뷰 작성 DTO
@Getter
@RequiredArgsConstructor
@Builder
public class ReviewWriteDTO {
// 리뷰 동작
private final ReviewKind reviewKind;
// 리뷰 내용
private final String contents;
// ... 그 외 필요한 객체
}
리뷰 작성 Interface
// review write service
public interface ReviewWriteService {
// 리뷰 작성
void reviewWrite(final ReviewWriteDTO dto) throws Exception;
}
각 동작별로 리뷰 작성 구현
// A 동작 review write service 구현
@Service
@Qualifier("AReviewWrite")
@RequiredArgsConstructor
public class AReviewWriteServiceImpl implements ReviewWriteService {
// A review write mapper
private final AReviewWriteMapper aReviewWriteMapper;
@Override
@Transactional
public void reviewWrite(final ReviewWriteDTO dto) throws Exception {
// ... A 동작 리뷰 작성 기능
}
}
// B 동작 review write service 구현
@Service
@Qualifier("BReviewWrite")
@RequiredArgsConstructor
public class BReviewWriteServiceImpl implements ReviewWriteService {
// B review write mapper
private final BReviewWriteMapper bReviewWriteMapper;
@Override
@Transactional
public void reviewWrite(final ReviewWriteDTO dto) throws Exception {
// ... B 동작 리뷰 작성 기능
}
}
// C 동작 review write service 구현
@Service
@Qualifier("CReviewWrite")
@RequiredArgsConstructor
public class CReviewWriteServiceImpl implements ReviewWriteService {
// C review write mapper
private final CReviewWriteMapper cReviewWriteMapper;
@Override
@Transactional
public void reviewWrite(final ReviewWriteDTO dto) throws Exception {
// ... C 동작 리뷰 작성 기능
}
}
// D 동작 review write service 구현
@Service
@Qualifier("DReviewWrite")
@RequiredArgsConstructor
public class DReviewWriteServiceImpl implements ReviewWriteService {
// D review write mapper
private final DReviewWriteMapper dReviewWriteMapper;
@Override
@Transactional
public void reviewWrite(final ReviewWriteDTO dto) throws Exception {
// ... D 동작 리뷰 작성 기능
}
}
3. 리뷰 작성 프록시 Service 객체 구현
지금까지 구현한 기능들을 이용하여 리뷰 작성 프록시 Service 객체를 구현한다. 리뷰 작성에 사용하는 ReviewWriteService를 이용하여 프록시 객체를 구현한다. 프록시 객체는 Service 의존성 주입 시 default로 사용할 객체이기 때문에 @Primary 어노테이션을 적용한다.
리뷰 작성 프록시 Service 객체는 다음과 같은 기능을 수행한다.
- 필요한 Service 의존성 주입
- 리뷰 작성 기능 수행 전 각 동작에 맞는 Service 객체 지연 생성
- 리뷰 작성
- 쿠폰 지급
- 포인트 지급
리뷰 작성 프록시 Service 객체 구현
// review write proxy service 구현
@Service
@Primary
@RequiredArgsConstructor
public class ReviewWriteProxyServiceImpl implements ReviewWriteService {
// A review write service
@Qualifier("AReviewWrite")
private final ReviewWriteService aReviewWriteService;
// B review write service
@Qualifier("BReviewWrite")
private final ReviewWriteService bReviewWriteService;
// C review write service
@Qualifier("CReviewWrite")
private final ReviewWriteService cReviewWriteService;
// D review write service
@Qualifier("DReviewWrite")
private final ReviewWriteService dReviewWriteService;
// coupon service
private final CouponService couponService;
// point service
private final PointService pointService;
@Override
@Transactional
public void reviewWrite(final ReviewWriteDTO dto) throws Exception {
// 지연 생성될 리뷰 작성 service 객체
ReviewWriteService reviewWriteService;
// 동작에 따른 리뷰 작성 service 생성
switch(dto.getReviewKind()) {
case A:
reviewWriteService = aReviewWriteService;
break;
case B:
reviewWriteService = aReviewWriteService;
break;
case C:
reviewWriteService = aReviewWriteService;
break;
case D:
reviewWriteService = aReviewWriteService;
break;
default:
throw new IllegalArgumentException("올바르지 않은 리뷰 작성 동작입니다.");
}
// 리뷰 작성
reviewWriteService.reviewWrite(dto);
// 쿠폰 지급
couponService.issueCoupon();
// 포인트 지급
pointService.issuePoint();
}
}
각각 최소 단위의 Service들을 조합하여 프록시 Service 객체를 구현하였다. 만약 리뷰 작성 동작이 추가된다면, 리뷰 작성 구현체를 하나 더 만들고 프록시 Service의 동작 조건문에 추가하기만 하면 된다. 만약 리뷰 작성 후 추가적인 동작이 필요하다면, 해당 프록시 객체에 기능을 추가하면 된다.
4. 리뷰 작성 Controller 구현
마지막으로 리뷰 작성 Controller를 구현한다.
리뷰 작성 Controller
@RestController
@RequiredArgsConstructor
public class ReviewWriteController {
// 리뷰 작성 Service
// @Primary 어노테이션이 붙어있는 프록시 Service 객체를 의존성 주입 받는다.
private final ReviewWriteService reviewWriteService;
@PostMapping("review")
public ResponseEntity<String> reviewWrite(@ResponseBody ReviewWriteDTO dto) {
// review write proxy service의 리뷰 등록 기능 호출
reviewWriteService.reviewWrite(dto);
// return
return ResponseEntity.ok()
.body("success");
}
}
코드에서 볼 수 있듯이, Controller 코드가 매우 단순하다. Service를 사용하는 개발자는 리뷰 작성에 대한 Service를 단순히 의존성 주입을 받아 사용하기만 하면 된다.
만약 리뷰 작성만을 사용하고 싶다면 다음과 같이 의존성 주입을 받으면 된다.
@RestController
@RequiredArgsConstructor
public class ReviewWriteController {
// A 리뷰 작성 Service
// @Qualifier 빈 구분자를 사용하여 의존성 주입
@Qualifier("AReviewWrite")
private final ReviewWriteService aReviewWriteService;
@PostMapping("review/a")
public ResponseEntity<String> reviewWrite(@ResponseBody ReviewWriteDTO dto) {
// A review write service의 리뷰 등록 기능 호출
aReviewWriteService.reviewWrite(dto);
// return
return ResponseEntity.ok()
.body("success");
}
}
정리하며
위에서 구현한 코드를 다이어그램으로 표현하면 다음과 같다.
재사용성을 증가시키고 관리하기 편하게끔 기능을 구현해보려 했다. 물론 최소 단위의 기능들을 따로 사용할 수도 있다.
사용자가 해당 기능을 최대한 편리하게 사용할 수 있게끔 구현해보려 했다. 코드를 사용하는 클라이언트 입장에서는 리뷰를 등록하는 service를 단순하게 의존성 주입을 받은 후, 리뷰 등록 메소드를 실행하기만 하면 된다. 내부적으로 어떤 기능이 동작하는지는 모른다.
기능 추가나 수정 시 편하게 작업이 가능하게 하면서, OCP 원칙을 지키도록 설계해보려 했다.
디자인 패턴을 공부할 때 단순히 좋은 패턴들이 있구나 생각했다. 그런데 실무를 진행하며 발생하는 문제를 해결하기 위한 수단으로 디자인 패턴이 유용하게 활용될 수 있다는 것을 느끼고 있다. 이론과 실무를 모두 알고 활용할 수 있다면 더 좋은 코드를 만들 수 있다고 생각한다