개발놀이터

추상화와 그 과정에서 무거워지는 책임 (SOLID 중 SRP) 본문

사이드 프로젝트/온라인 쇼핑몰 ver.5

추상화와 그 과정에서 무거워지는 책임 (SOLID 중 SRP)

마늘냄새폴폴 2024. 4. 27. 23:23

오늘도 사이드 프로젝트를 리팩토링하면서 시간을 보내고 있었습니다. 얼마전 if-else 블록의 중복으로인해 추상화하는 방법을 깨달아서 이 방법으로 추상화를 진행했습니다. 

 

그 결과 이렇게 코드를 줄일 수 있었죠. 

 

 

이랬던 코드들이

 

 

이렇게 바뀌었습니다. 

 

https://coding-review.tistory.com/487

 

다형성을 이용해 if else 블럭 추상화하기

취준때 개발했던 온라인쇼핑몰 프로젝트는 약 2년전에 개발한만큼 돌아가게만 만든 경향이 있는 코드들입니다. 읽기 힘든 코드는 물론이고 확장성을 고려하지않은 구조가 많았습니다. 제 프로

coding-review.tistory.com

 

리팩토링 방법은 위의 링크와 같은 방식으로 처리했습니다. 

 

리팩토링으로 SRP와 OCP를 동시에 지킬 수 있게 되었습니다. 저는 객체지향 프로그래밍에서 가장 중요한 것은 SRP와 OCP 그리고 그 다음이 ISP 그 다음이 LSP와 DIP라고 생각합니다. 

 

왜냐하면 DIP는 대부분의 프레임워크가 구현해주는 것이라 크게 의미 없다고 생각하고, LSP는 상속과 관련된 원칙이라 아직 상속을 강하게 사용한 적이 없는 저는 딱히 중요성을 느끼지 못했습니다. 

 

ISP는 인터페이스를 사용하다보면 점점 인터페이스를 쪼개야하는 상황이 오게 되는데 이때 종종 상기하면서 적용하는 부분입니다. 

 

때문에 단일 책임 원칙 (SRP), 개방 폐쇄 원칙 (OCP) 이 두개만 지켜주면 웬만한 코드는 다 유연해지고 확장성이 높아집니다. 

 

위의 코드 예시에서도 리팩토링 전에는 lookUpPayment라는 결제 부분에서 역할 (MemberRole)에 따라 로직이 다르기 때문에 역할을 구분짓는 책임 (if - else 블록) 그리고 결제하는 책임 (원래 본인책임) 이렇게 두개가 혼재되어있는 상황이었습니다. 

 

또한, 새로운 정책이 추가되는 경우 (물론 그럴일은 없지만), 예를 들어 Manager가 추가된다면 결제 로직에 또 다시 if 문을 넣어서 추가해줘야하기 때문에 OCP에도 위배됩니다. 

 

그래서 리팩토링을 진행했고 역할에 따라 다른 정책을 부여하는 PolicyManager라는 클래스로 책임을 분리했습니다. 그리고 결제 로직에선 결제만 진행하도록 SRP와 OCP를 모두 지켰습니다. 

 

그러는 과정에서 고민이 생겼습니다. 

 

@Component
@RequiredArgsConstructor
public class PolicyManager {

    // dependency for all
    private final CacheRepository cacheRepository;

    // dependency for coupon
    private final MemberWhoGetCouponRepository memberWhoGetCouponRepository;
    private final CouponRepository couponRepository;
    private final TemporaryOrderService temporaryOrderService;

    // dependency for payment
    private final OrderItemRepository orderItemRepository;
    private final BucketRepository bucketRepository;
    private final TemporaryOrderRepository temporaryOrderRepository;
    private final ItemDetailRepository itemDetailRepository;

    private final CheckStockUtils checkStockUtils;

    public CouponPolicy couponPolicy(MemberRole role) {
        if (role.equals(MemberRole.ROLE_SOCIAL)) {
            return new UserCouponPolicy(cacheRepository, memberWhoGetCouponRepository);
        }
        else {
            return new MemberCouponPolicy(cacheRepository, memberWhoGetCouponRepository, couponRepository, temporaryOrderService);
        }
    }

    public PaymentPolicy paymentPolicy(MemberRole role) {
        if (role.equals(MemberRole.ROLE_SOCIAL)) {
            return new UserPaymentPolicy(cacheRepository, orderItemRepository, bucketRepository, temporaryOrderRepository, itemDetailRepository, memberWhoGetCouponRepository, checkStockUtils);
        }
        else {
            return new MemberPaymentPolicy(cacheRepository, orderItemRepository, bucketRepository, temporaryOrderRepository, itemDetailRepository, memberWhoGetCouponRepository, checkStockUtils);
        }
    }
}

 

PolicyManager의 코드입니다. 

 

PolicyManager의 책임이 굉장히 무거워진 것을 느꼈습니다. 만약 할인이라던가, 환불이라던가 이런 정책들이 추가되면 PolicyManager의 책임이 지금보다 더 무거워질 것입니다. 

 

GPT는 전략패턴을 이용해서 PolicyManager를 한번 더 추상화하라고 추천해줬습니다. 

 

하지만 아직 그렇게 리팩토링을 하진 않았습니다. 이 고민은 조금 더 생각해보고 결정하려고 합니다. 그렇게 생각한 이유는 다음과 같습니다. 

 

  1. 요즘 GPT가 멍청하다. 
    이 부분은 동의하실지 모르겠지만 앞전에 했던 얘기를 까먹는다거나, 내 질문을 잘못 이해하고 그게 아니라고 설명해줘도 또 같은 말을 반복하는 상황이 요즘들어 잦아졌습니다. 

    GPT5를 더 똑똑하다고 생각하게 만들기위해 GPT4의 성능을 낮추는 것이 아닌가싶을 정도로 GPT4를 처음 사용했던 1년전과 다른 느낌을 받았습니다. 
  2. 추상화를 한번 더?
    만약 제 코드를 처음 보는 사람이 결제 로직을 확인하고 싶어서 리팩토링된 깔끔한 코드에서 구현체로 들어가고싶은 상황이라고 가정해보도록 하겠습니다. 

    그럼 총 세번의 이동이 필요합니다. PaymentService => 추상화된 PolicyManager (한번) => PolicyManager (두번) => 구현체인 lookUpPayment (세번) 로 들어갑니다. 

    저는 세번 이상 들어가는 것을 지양하는 코드 컨벤션을 가지고 있습니다. 다시 돌아올 때 굉장히 힘들더라구요.. 
  3. 책임이 무거워지는 것이지 SRP를 위배하진 않는다. 
    책임이 무거운 것은 맞습니다. 하지만 여전히 PolicyManager는 정책을 정하는 책임 단 한가지만 가지고 있습니다. 

    이걸 추상화한다고 한들 코드를 읽기 쉬워질까? 객체를 지향하고 있나? 이에 대한 답을 뚜렷하게 낼 수 없었습니다.
  4. 무거워진다고 뭐가 문제라도?
    문제는 없습니다. 오히려 DIP를 계속 지키고 있는 것이죠.

 

마치며

 

이 부분은 조금 더 공부를 하고 어떻게 해결해야할지 고민해보겠습니다. 애초에 해결해야하는 문제인가에 대해서도 진지하게 고민해보겠습니다.