개발놀이터

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

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

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

마늘냄새폴폴 2024. 4. 6. 23:50

취준때 개발했던 온라인쇼핑몰 프로젝트는 약 2년전에 개발한만큼 돌아가게만 만든 경향이 있는 코드들입니다. 

 

읽기 힘든 코드는 물론이고 확장성을 고려하지않은 구조가 많았습니다. 

 

제 프로젝트에서 문제가 된 부분을 단순화해서 보여드리고 어떻게 리팩토링 하였는지 포스팅해보려고합니다. 

 

 

if else 블럭을 추상화?

한번 이런 상황을 가정해보도록 하겠습니다. 

 

우리 프로젝트는 관리자 (ADMIN), 매니저 (MANAGER), 일반회원 (MEMBER)에 따라 할인이나 결제에 대한 정책이 다르게 설정되어있습니다. 

 

때문에, 할인정책에서도 관리자, 매니저, 일반회원인지를 확인하고 정책을 적용해야하고 결제정책 또한 마찬가지입니다. 그 결과 제 코드는 이런 방식으로 짜게되었습니다. 

 

@Service
public class PaymentService {

    public void payment(MemberRole role) {
        if (role.equals(MemberRole.ROLE_ADMIN)) {
            // payment logic for admin 
        }
        else if (role.equals(MemberRole.ROLE_MEMBER)) {
            // payment logic for manager
        }
        else {
            // payment logic for member
        }
    }
}

 

@Service
public class DiscountService {

    public void discount(MemberRole role) {
        if (role.equals(MemberRole.ROLE_ADMIN)) {
            // discount logic for admin 
        }
        else if (role.equals(MemberRole.ROLE_MANAGER)) {
            // discount logic for manager
        }
        else {
            // discount logic for member
        }
    }
}

 

 

이런 코드를 우리 프로젝트에 처음 투입된 사람이 본다면 어떨까요? 직관적이긴 할겁니다. 아 할인정책을 역할에 맞게 적용하고, 결제정책도 역할에 맞게~ 음음 이해했어. 

 

 

하지만 이런 상황은 문제가 있습니다. 

 

  1. if else 블럭 안에 있는 로직이 길어지면 이해하기 힘들다. (심각도 : 낮음)
  2. if else 문이 사실 안에 내용만 다르고 조건문은 항상 고정 => if else 문이 중복됨 (심각도 : 중간)
  3. 만약 새로운 역할이 추가되면 모든 정책에 else if 문을 추가하여 코드를 수정해야함 (심각도 : 높음)

 

특히 3번이 왜 문제인가 살펴보면 객체지향의 5대원칙 SOLID 중 OCP를 위배하는 상황입니다. 

 

 

리팩토링

이제 이 상황을 해결해보도록하겠습니다. 

 

먼저 모든 정책을 관리하는 인터페이스를 하나 만들어줍니다. 

 

public interface Policy {

    void discountPolicy();

    void paymentPolicy();

}

 

 

그리고 각 역할에 맞는 구현체를 만들어줄겁니다. 

 

@Slf4j
public class AdminPolicy implements Policy {
    @Override
    public void discountPolicy() {
        log.info("admin discount policy call");
    }

    @Override
    public void paymentPolicy() {
        log.info("admin payment policy call");
    }

}

 

@Slf4j
public class ManagerPolicy implements Policy {
    @Override
    public void discountPolicy() {
        log.info("manager discount policy call");
    }

    @Override
    public void paymentPolicy() {
        log.info("manager payment policy call");
    }

}

 

@Slf4j
public class MemberPolicy implements Policy {

    @Override
    public void discountPolicy() {
        log.info("member discount policy call");
    }

    @Override
    public void paymentPolicy() {
        log.info("member payment policy call");
    }

}

 

 

그리고 각 Service 코드들을 수정해보겠습니다. 

 

@Service
public class PaymentService {

    public void payment(MemberRole role) {
        Policy policy = getPolicy(role);
        policy.paymentPolicy();
    }

    private Policy getPolicy(MemberRole role) {
        if (role.equals(MemberRole.ROLE_ADMIN)) {
            return new AdminPolicy();
        }
        else if (role.equals(MemberRole.ROLE_MANAGER)) {
            return new ManagerPolicy();
        }
        else {
            return new MemberPolicy();
        }
    }
}

 

@Service
public class DiscountService {

    public void discount(MemberRole role) {
        Policy policy = getPolicy(role);
        policy.discountPolicy();
    }

    private Policy getPolicy(MemberRole role) {
        if (role.equals(MemberRole.ROLE_ADMIN)) {
            return new AdminPolicy();
        }
        else if (role.equals(MemberRole.ROLE_MANAGER)) {
            return new ManagerPolicy();
        }
        else {
            return new MemberPolicy();
        }
    }
}

 

자 그런데 getPolicy도 결국 각 역할에 맞게 Policy를 선택하는거 아니겠습니까? 그럼 이것도 스프링 빈으로 관리해보도록 하겠습니다. 

 

@Component
public class PolicyManager {


    public Policy policy(MemberRole role) {
        if (role.equals(MemberRole.ROLE_ADMIN)) {
            return new AdminPolicy();
        }
        else if (role.equals(MemberRole.ROLE_MANAGER)) {
            return new ManagerPolicy();
        }
        else {
            return new MemberPolicy();
        }
    }

}

 

그럼 코드가 이렇게 바뀝니다. 

 

@Service
@RequiredArgsConstructor
public class DiscountService {

    private final PolicyManager policyManager;

    public void discount(MemberRole role) {
        Policy policy = policyManager.policy(role);
        policy.discountPolicy();
    }
}

 

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PolicyManager policyManager;

    public void payment(MemberRole role) {
        Policy policy = policyManager.policy(role);
        policy.paymentPolicy();
    }
}

 

의존성은 기존엔 이렇게

 

리팩토링 후 이렇게 바뀌었습니다.  

 

 

이렇게 리팩토링이 끝나게됩니다. 

 

이렇게 추상화하는 방식에는 장단점이 있습니다. 

 

  • 장점
    • 의존성을 분리할 수 있습니다. 이말은 Service 로직은 해당 비즈니스 로직만 호출하는 것이지 역할에 따라 비즈니스 로직을 다르게 적용하는 것은 Policy 라는 인터페이스에게 위임하는 것이죠. 이로인해 Service 계층은 PolicyManager가 뭐하는 친구인지 몰라도됩니다. 
    • 만약 새로운 정책, 예를 들어 환불정책이 추가된다면 Policy 인터페이스에서 refundPolicy() 라는 메서드만 추가해주고 각 구현체에서 구현해주기만하면 됩니다. 
    • 만약 새로운 역할이 추가된다면 Policy 를 의존하는 새로운 클래스를 추가하여 관리하면 됩니다. 만약 그렇게 된다면 Service 로직에는 그 어떤 변화도 주지 않고 확장할 수 있는 것이죠. 
  • 단점
    • 일단 인터페이스로 구현되어있는걸 보게되면 속이 답답해집니다. 어디서 어떤 구현체를 쓰는지 찾아야되는 상황이 오죠... 이 코드를 제가 구현했으면 어디서 어떤 로직을 호출하는지 다 알고있으니 괜찮은데 만약 이 프로젝트에 새로운 사람이 투입된다면 이 코드를 이해하기위해 리팩토링 이전보다 더 많은 시간을 사용해야 할 것입니다. 
    • 예를들어 새로운 정책인 환불정책이 추가된다면 인터페이스에 refundPolicy() 라는 메서드가 추가될 것이고 그럼 인터페이스의 구현체인 각 역할에서 환불에 대한 추가적인 코드 작업이 있어 개발하는 속도가 굉장히 느려집니다. 

 

이 상황은 어느 것이 더 좋다라고 딱 잘라 말하기 힘들겁니다. 일단 코드가 깔끔해보이고 유지보수하기 좋은 쪽은 후자일 것 같긴합니다. 또한, 객체지향의 원칙을 딱딱 지켜 만들었기 때문에 조금 더 이상적인 코드라고 할 수 있겠네요.

 

하지만 빠르게 마감시간에 맞춰야하거나 역할이나 정책같이 추가될 수 있는 것이 아니라 죽었다 깨어나도 추가될 일 없는 것들은 굳이 추상화할 필요가 없죠. 

 

때문에 이런 경우 개발 초기엔 빠르게 개발하는 것이 중요하기 때문에 리팩토링 전처럼 개발하고 후에 확장성이 중요해질 때 리팩토링 하는 것이 좋을 것 같습니다.