개발놀이터

리팩토링 (2-1) : switch 문 인터페이스, 추상클래스로 리팩토링 본문

리팩토링

리팩토링 (2-1) : switch 문 인터페이스, 추상클래스로 리팩토링

마늘냄새폴폴 2023. 7. 23. 18:31

이번 포스팅에선 책 클린코드에서 나온 예제인 switch문을 인터페이스, 추상클래스로 리팩토링 하는 방법에 대해서 포스팅해보도록 하겠습니다. 

 

사실 책에서는 두루뭉실하게 얘기하기 때문에 조금 와닿지 않아서 개인적으로 토이프로젝트를 한번 만들어 봤습니다. 

 

추상클래스와 인터페이스는 최근에서야 온전히 이해하기 시작해서 조금 미숙할 수 있습니다. 

    public Money calculatePay(Employee e) throws InvalidEmployeeType {
        switch (e.type) {
            case COMMISSTION -> {
                return calculateCommisstionPay(e);
            }
            case HOURLY -> {
                return calculateHourlyPay(e);
            }    
            case SALARIED -> {
                return calculateSalariedPay(e);
            }
            default -> {
                throw new InvalidEmployeeType(e.type);
            }
        }
    }

이런 switch문이라면 다형성을 이용하기 딱 좋겠다는 생각을 책을 읽으며 했습니다. 하지만 인터페이스를 바로 떠올렸지 추상클래스를 떠올리진 못했습니다. 

 

하지만 책에서 이런 경우 추상클래스로 한번 감싸는 것이 좋다는 얘기를 하였으니 한번 떠오르는대로 리팩토링 해보도록 하겠습니다. 

 

비즈니스 요구사항

  1. 우리 서비스는 외주, 시간제, 월급제 고용인을 쓰고 있습니다.
  2. 각각 봉급을 지급하는 날짜가 다릅니다.
  3. 각각 봉급을 계산하는 방식이 다릅니다. (ex. 외주 = 건당, 시간제 = 시간당, 월급제 = 월당)
  4. 외주, 시간제, 월급제 고용인은 각각 다른 데이터베이스에 저장되어 있습니다. 

 

상황

  • 비즈니스 로직에 대한 직접적인 구현은 하지 않았습니다. 괜히 프로젝트가 커지는 것은 원하지 않았습니다. 
  • 모든 비즈니스 로직은 로그로 대체합니다. 
  • 스프링 프레임워크를 사용하지 않았습니다. (실제 사용은 어떤 방식으로 할건지도 생각해 봤습니다. 하지만 토이 프로젝트에선 순수 자바로 다형성을 구현해봤습니다.)

 

우선 구조는 이렇게 되어있습니다. 

 

Employee.java

public abstract class Employee {

    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}

 

calculatePay의 리턴값으로 받는 Money 클래스는 구현하지 않고 빈껍데기로 냅뒀습니다. 

Money.java

public class Money {
}

 

EmployeeFactory.java

public interface EmployeeFactory {

    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

 

EmployeeFactoryImpl.java

public class EmployeeFactoryImpl implements EmployeeFactory {
    @Override
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r) {
            case COMMISSION -> {
                return new CommissionedEmployee(r);
            }
            case HOUR -> {
                return new HourlyEmployee(r);
            }
            case SALARY -> {
                return new SalariedEmployee(r);
            }
            default -> {
                throw new InvalidEmployeeType("type is " + r);
            }
        }
    }
}

EmployeeRecord.java

public enum EmployeeRecord {

    COMMISSION, HOUR, SALARY, INVALID
}

 

만약 구현체인 EmployeeFactorymakeEmployee를 호출하면 (현재 구현체는 EmployeeFactoryImpl) 추상클래스를 구현한 CommissionedEmployee, HourlyEmployee, SalariedEmployee가 작동합니다. 

 

이제 추상클래스의 구현체를 보시죠. 

CommissionedEmployee.java

@Slf4j
public class CommissionedEmployee extends Employee {

    private String record;

    public CommissionedEmployee(EmployeeRecord record) {
        this.record = record.name();
    }

    @Override
    public boolean isPayday() {
        System.out.println("커미션 고용인의 페이 날짜 계산 로직, 고용인 등급 : " + record);
        return false;
    }

    @Override
    public Money calculatePay() {
        System.out.println("커미션 고용인의 페이 계산 로직, 고용인 등급 : " + record);
        return null;
    }

    @Override
    public void deliverPay(Money pay) {
        System.out.println("커미션 고용인으로 계좌 송금, 고용인 등급 : " + record);
    }
}

HourlyEmployee.java

@Slf4j
public class HourlyEmployee extends Employee {

    private String record;

    public HourlyEmployee(EmployeeRecord record) {
        this.record = record.name();
    }

    @Override
    public boolean isPayday() {
        System.out.println("시간제 고용인의 페이 날짜 계산 로직, 고용인 등급 : " + record);
        return false;
    }

    @Override
    public Money calculatePay() {
        System.out.println("시간제 고용인의 페이 계산 로직, 고용인 등급 : " + record);
        return null;
    }

    @Override
    public void deliverPay(Money pay) {
        System.out.println("시간제 고용인으로 계좌 송금, 고용인 등급 : " + record);
    }
}

SalariedEmployee.java

@Slf4j
public class SalariedEmployee extends Employee {

    private String record;

    public SalariedEmployee(EmployeeRecord record) {
        this.record = record.name();
    }

    @Override
    public boolean isPayday() {
        System.out.println("월급제 고용인의 페이 날짜 계산 로직, 고용인 등급 : " + record);
        return false;
    }

    @Override
    public Money calculatePay() {
        System.out.println("월급제 고용인의 페이 계산 로직, 고용인 등급 : " + record);
        return null;
    }

    @Override
    public void deliverPay(Money pay) {
        System.out.println("월급제 고용인으로 계좌 송금, 고용인 등급 : " + record);
    }
}

 

각각 외주, 시간제, 월급제 고용인에 대한 비즈니스 로직이 설정되어있습니다. 

 

이제 실행해보도록 하겠습니다. 

 

실행

@Slf4j
public class Main {

    public static void main(String[] args) {
        EmployeeFactory factory = new EmployeeFactoryImpl();
        try {
            Employee employee = factory.makeEmployee(EmployeeRecord.COMMISSION);
            employee.isPayday();
            employee.calculatePay();
            employee.deliverPay(null);
        } catch (InvalidEmployeeType e) {
            log.error("Invalid Employee Record, Record : {}", EmployeeRecord.INVALID);
            throw new RuntimeException(e);
        }
    }
}

지금은 순수 자바 코드로 구현했기 때문에 main 메서드에서 실행해보도록 하겠습니다. 인터페이스인 EmployeeFactory의 구현체로 EmployeeFactoryImpl을 선택한 다음 makeEmployeeCOMMISSION으로 호출해주면?

 

 

makeEmployeeenum 타입을 바꿔주기만 하면 다형성에 의해 구현체가 슥삭 바뀌는 모습을 볼 수 있습니다. 

 

다형성을 적극적으로 이용해봤습니다. 

 

이제 실전에선 어떻게 사용해야하는지 알아보죠. 

 

실전

@Configuration
public class IfUseSpringEmployeeConfig {

    @Bean
    public EmployeeFactory employeeFactory() {
        return new EmployeeFactoryImpl();
    }
}

저는 수동으로 빈을 등록해줬습니다. 컴포넌트 스캔을 사용할 수도 있었지만 저는 다형성을 이용해야할 때는 수동으로 빈을 등록하는게 OCP를 최대한 위반하지 않는다고 생각하기 때문에 수동으로 빈을 등록해줬습니다. 

 

물론 RepositoryService, Controller 등을 빈으로 등록할 때는 컴포넌트 스캔을 사용하는 것이 조금 더 편합니다. 

 

제 생각엔 변경이 자주 있는 경우엔 수동으로, 변경을 자주하지 않는 경우엔 컴포넌트 스캔을 사용하는 것이 좋다고 생각합니다. 

 

그리고 사용할 곳에서 EmployeeFactory를 주입받으면 빈으로 등록한 EmployeeFactoryImpl이 선택될 것입니다. 

 

UseSpringFrameworkMain.java

@Service
@RequiredArgsConstructor
public class UseSpringFrameworkMain {

    private final EmployeeFactory employeeFactory;

    public void employeeCheck() {
        try {
            Employee employee = employeeFactory.makeEmployee(EmployeeRecord.COMMISSION);
            employee.isPayday();
            employee.calculatePay();
            employee.deliverPay(null);
        } catch (InvalidEmployeeType e) {
            throw new RuntimeException(e);
        }
    }
}

생성자 주입으로 빈을 주입받았습니다. 제가 스프링에 의해 주입받은 것은 인터페이스이기 때문에 추상클래스 (인터페이스) 에 의존하고 구현클래스엔 의존하지 않는다는 DIP에 위배되지 않습니다. 

 

이제 다시 우리가 만든 Service를 빈으로 등록하고 employeeCheck를 호출하면 우리가 main 메서드에서 동작했던 것과 똑같이 동작하게 됩니다. 

 

 

마치며

인프런의 영한님 강의를 보면 위와 비슷한 방식으로 자바의 다형성을 이용해 OCP와 DIP를 철저하게 지키는 강의를 볼 수 있습니다. 

 

강의에선 추상클래스를 사용하진 않지만 제가 구현한 내용들은 전부 영한님의 강의에서 하던 방식과 정말 유사합니다. 

 

제 포스팅을 보시고 이해가 조금 부족하다고 생각하신다면 영한님의 스프링 핵심 원리 - 기본편 을 참고해주세요! 

 

이렇게 다형성을 이용해 클린코드를 구축해봤습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

'리팩토링' 카테고리의 다른 글

JWT 필터 리팩토링  (0) 2023.07.23
리팩토링 (2) : 함수  (0) 2023.07.22
리팩토링 (1) : 의미있는 이름  (0) 2023.07.22
리팩토링 (0) : 개요  (0) 2023.07.22