본문 바로가기
디자인 패턴

PayrollSystem 프로젝트에서 디자인 패턴은 어떻게 사용되었는가?(feat. 클린 소프트웨어)

by simplify-len 2020. 9. 9.

들어가기

'클린 소프트웨어 - 로버트 마틴 저자' 에서 payroll 아키텍처 코드를 보고...
Slipp 스터디에서 디자인패턴에 대한 학습을 하면서, 클린 소프트웨어 책을 학습하고 있습니다.
(이 책의 좋은 점은 따로 또 포스팅하겠습니다.) 
 책의 일부분으로 로버트마틴이 PayrollSystem 코드가 나오는데, 그 부분을 보고 이해한 부분을 적어볼까합니다.

어떻게 만들어졌을가?

이 책에서는 총 5가지의 명세가 나오고 이를 해결하기 위해 

다음과 같은 패턴을 활용합니다.

1. 커맨드 패턴와 액티브 오브젝트 패턴

2. 템플릿 메서드와 스트레티지 패턴

3. 퍼사드 패턴

4. 싱글톤과 모노스테이트 패턴

5. 널 오브젝트 패턴

코드에서 어떻게 사용되고 있는지 확인해봅시다.

각 패턴에 대한 자세한 설명은 스킵하고, 필요한 부분만 설명하겠습니다.

 

코드로서 PayrollSystem 은?

시스템의 시작은 Transaction의 Interface로 시작합니다.

public interface Transaction {
        
        void execute();
}

 

그리고 Transaction 은 Command 패턴으로 이어집니다. 
Command 패턴은 명령의 개념을 캡슐화함으로써 연결된 장치에서 시스템의 논리적인 상호 연결을 분리하게 만듭니다.

코드로서 살펴보면 

@Test
public void testAddSalariedEmployee(){
  int employeeId = 1;
  Transaction addEmployeeTransaction =
  new AddSalariedEmployeeTransaction(employeeId, "Bob", "Home", 1000.0);
  addEmployeeTransaction.execute();

  Employee employee = databaseResource.getInstance().getEmployee(employeeId);
  assertThat(employee.getName(), is("Bob"));

  PaymentClassification paymentClassification = employee.getPaymentClassification();
  assertThat(paymentClassification, is(instanceOf(SalariedClassification.class)));
  SalariedClassification salariedClassification = (SalariedClassification) paymentClassification;
  assertThat(salariedClassification.getSalary(), closeTo(1000.0, FLOAT_ACCURACY));

  assertThat(employee.getPaymentSchedule(), is(instanceOf(MonthlyPaymentSchedule.class)));
  assertThat(employee.getPaymentMethod(), is(instanceOf(HoldMethod.class)));
  assertThat(employee.getUnionAffiliation(), is(UnionAffiliation.NO_AFFILIATION));
}

 

Transaction 이 이벤트를 주도적으로 이끄는 역할을 합니다.

이렇게 하면 좋은 점이 뭘까요? 위 코드에서 보면 addEmployeeTransaction 객체를 생성하는 부분과 .execute() 를 분리하는 효과를 내고 있습니다. 이렇게 될 경우, 데이터를 받아 객체를 생성하는 것과, 실제로 실행되는 코드를 분리함으로써- 책에서는 물리적, 시간적으로 분리한다 라고 표현하고 있습니다.

다시 코드로 돌아가, Transaction 을 Command 으로 활용합니다.

 

다음으로는 템플릿 메소드 패턴을 활용해, 객체를 생성하는 부분을 살펴보겠습니다.

템플릿 메소드 패턴은 구현체가 인터페이스를 바라보는 방향이 아닌, 반대로 인터페이스가 구현체를 바라보는 방향으로 의존성을 역전시켜 코드의 확장성을 열어줍니다. 동시에, 템플릿 메소드 패턴은, 상위 클래스에 비지니스 로직을 모아두기 때문에, 하위 클래스는 상위 클래스가 무엇을 하는지 알 필요가 없습니다. 단순히 상위 클래스에서 시키는것만 구현하면 되는거죠.

 이 부분도 바로 코드로 들어가볼까요?

public abstract class AddEmployeeTransaction implements Transaction {

    private final int employeeId;
    private final String employeeName;
    private final String employeeAddress;

    public AddEmployeeTransaction(int employeeId, String employeeName, String employeeAddress){
        this.employeeId = employeeId;
        this.employeeName = employeeName;
        this.employeeAddress = employeeAddress;
    }

    @Override
    public void execute() {
        PaymentClassification paymentClassification = getPaymentClassification();
        PaymentSchedule paymentSchedule = getPaymentSchedule();
        PaymentMethod paymentMethod = new HoldMethod();
        Employee employee = new Employee(employeeId, employeeName, employeeAddress);
        employee.setPaymentClassification(paymentClassification);
        employee.setPaymentSchedule(paymentSchedule);
        employee.setPaymentMethod(paymentMethod);
        PayrollDatabase.globalPayrollDatabase.addEmployee(employeeId, employee);
    }

    protected abstract PaymentSchedule getPaymentSchedule();

    protected abstract PaymentClassification getPaymentClassification();
}

 

 위 Transactions 을 구현하는 추상클래스인 AddEmployeeTransaction 입니다.  getPaymentSchedule() , getPaymentClassification() 이 추상으로 놓여져있는 것을 볼 수 있는데요. addEmployeeTransaction이 정확하게 동작하기 위해서는 아래 2개의 메소드가 구현되어야만 합니다. 그러므로, 위에서 말씀드린것과 같이 의존성의 방향을 역전시켜, 간결한 의존성을 만들고, 이는 곧 확장에 용이하도록 만들어지는 효과를 냅니다.

그럼 템플릿 메소드 패턴의 단점은 무엇일까?

템플릿 메소드의 패턴의 단점으로는, 조합의 폭발이 일어날 수 있습니다.
자세한 내용은 이 곳을 확인해주세요

이번에는 퍼사드 패턴을 활용한 예시를 살펴보겠습니다.

해당 프로젝트에서는 퍼사드 패턴을 PayrollDatabase.java 에서 사용되었습니다. 퍼사드 패턴이란? '건물의 정면' 이라는 뜻으로, 위키백과에 나와있는 대표적인 예시를 살펴보겠습니다.

/* Complex parts */

class CPU {
	public void freeze() { ... }
	public void jump(long position) { ... }
	public void execute() { ... }
}

class Memory {
	public void load(long position, byte[] data) {
		...
	}
}

class HardDrive {
	public byte[] read(long lba, int size) {
		...
	}
}

/* Façade */

class Computer {
	public void startComputer() {
        CPU cpu = new CPU();
        Memory memory = new Memory();
        HardDrive hardDrive = new HardDrive();
		cpu.freeze();
		memory.load(BOOT_ADDRESS, hardDrive.read(BOOT_SECTOR, SECTOR_SIZE));
		cpu.jump(BOOT_ADDRESS);
		cpu.execute();
	}
}

/* Client */

class You {
	public static void main(String[] args) throws ParseException {
		Computer facade = /* grab a facade instance */;
		facade.startComputer();
	}
}

여기서는 Computer 클래스가 퍼사드 역할을 합니다.

PayrollSystem에서는 아래와 같은 코드가 있습니다.

public class PayrollDatabase {

    public static PayrollDatabase globalPayrollDatabase = new PayrollDatabase();

    private Map<Integer, Employee> employees = new HashMap<Integer, Employee>();
    public Map<Integer, Employee> unionMembers = new HashMap<Integer, Employee>();


    public Employee getEmployee(int employeeId) {
        return employees.get(employeeId);
    }

    public void addEmployee(int employeeId, Employee employee) {
        employees.put(employeeId, employee);
    }

    public void clear(){
        employees.clear();
        unionMembers.clear();
    }

    public void deleteEmployee(int employeeId) {
        employees.put(employeeId, null);
    }

    public Employee getUnionMember(int memberId) {
        return unionMembers.get(memberId);
    }

    public void addUnionMember(int memberId, Employee employee) {
        unionMembers.put(memberId, employee);
    }

    public void deleteUnionMember(int memberId) {
        unionMembers.remove(memberId);
    }

    public Set<Integer> getAllEmployeeIds() {
        return employees.keySet();
    }
}

 

여기서 어디가 퍼사드패턴이냐? 라고 물을 수 있지만, 지금 위 코드에서는 DB를 Dictionary 객체로 대체했지만, 실제 DB와 연동하는 JDBC, JPA와 같은 도구가 활용되었다라면, getEmployee(int employeeId), addEmployee(int employeeId, Employee employee) 는 도구에 따른 연동 도구가 들어가 퍼사프 패턴을 이룰 것입니다.

이번에는 전략 패턴에 대해서 한번 살펴봅시다.

payrollsystem에서는 PayDay()가 전략패턴을 활용하고 있는데, 어떻게 활용되고 있는가를 살펴보겠습니다.

package payrollcasestudy.entities;

import payrollcasestudy.entities.affiliations.UnionAffiliation;
import payrollcasestudy.entities.paymentclassifications.PaymentClassification;
import payrollcasestudy.entities.paymentmethods.PaymentMethod;
import payrollcasestudy.entities.paymentschedule.PaymentSchedule;

import java.util.Calendar;

public class Employee {

        private PaymentClassification paymentClassification;
        private PaymentSchedule paymentSchedule;
        private PaymentMethod paymentMethod;
...
        private UnionAffiliation unionAffiliation = UnionAffiliation.NO_AFFILIATION;

        public Employee(int employeeId, String name, String address) {
                this.employeeId = employeeId;
                this.name = name;
                this.address = address;
        }

        public PaymentClassification getPaymentClassification() {
                return paymentClassification;
        }

        public void setPaymentClassification(PaymentClassification paymentClassification) {
                this.paymentClassification = paymentClassification;
        }

        public void setPaymentSchedule(PaymentSchedule paymentSchedule) {
                this.paymentSchedule = paymentSchedule;
        }

        public void setPaymentMethod(PaymentMethod paymentMethod) {
                this.paymentMethod = paymentMethod;
        }

...
        public void setUnionAffiliation(UnionAffiliation unionAffiliation) {
                this.unionAffiliation = unionAffiliation;
        }
        
        ...
        
        public UnionAffiliation getUnionAffiliation() {
                return unionAffiliation;
        }

        public boolean isPayDate(Calendar payDate) {
                return paymentSchedule.isPayDate(payDate);
        }

        public Calendar getPayPeriodStartDay(Calendar payDate) {
                return paymentSchedule.getPayPeriodStartDate(payDate);
        }

        public void payDay(PayCheck payCheck) {
                double grossPay = paymentClassification.calculatePay(payCheck);
                double deductions = unionAffiliation.calculateDeduction(payCheck);
                double netPay = grossPay - deductions;
                payCheck.setGrossPay(grossPay);
                payCheck.setDeductions(deductions);
                payCheck.setNetPay(netPay);
                paymentMethod.pay(payCheck);
        }

}

 가장 아래의 payDay method 을 유심히 살펴보겠습니다.

인터페이스 

PaymentClassification
PaymentSchedule
PaymentMethod

위 3개가 어떤 것을 받는가에 따라 계산 방식이 달라집니다. 즉, 인터페이스를 사용한 간결한 결합으로- 전략패턴을 구현한 것입니다.

System에서는 아래와 같은 구현체를 갖고 있습니다. 

구현체들

 인터페이스의 장점이 다시한번 눈에 띄게 보이네요. 이래서 잘 만든 인터페이스 하나가 10 클래스 부럽지 않다라고 하나봅니다.

책에서는 싱글톤과 모노스테이트에 대해서 언급하고, 이는 곧 payrolldatabase.java에서 

public class PayrollDatabase {

    public static PayrollDatabase globalPayrollDatabase = new PayrollDatabase();

	...
}

같은 형태의 사용을 싱글톤 패턴을 언급하지만, 저자가 말하길, 불필요한 복잡성의 악취를 풍길 수 있다라고 이야기하고 있습니다. 그러므로, 싱글톤과 모노스테이트 패턴은 넘어가도록 하겠습니다.

마지막으로 널 오브젝트 패턴에 대한 설명으로 Payrollsystem의 디자인 패턴 관점에서의 설명을 마무리하겠습니다.

널 오브젝트 패턴에 대해서 잘 설명해 주신 기계인간 존립의 블로그 첨부합니다. 

널 오브젝트 패턴은 단순히 Java의 악이라 불리는 Null을 피하기 위한 하나의 수단으로 동작합니다.

이번에도 마찬가지로 코드로서 살펴보면 아래와 같습니다.

public class UnionAffiliation {

    public static final UnionAffiliation NO_AFFILIATION = new UnionAffiliation(-1,0);
    
    private final double dues;
    private final Map<Calendar, ServiceCharge> serviceCharges = new HashMap<Calendar, ServiceCharge>();
    private final int memberId;

    public UnionAffiliation(int memberId, double dues) {
        this.memberId = memberId;
        this.dues = dues;
    }
    ...
}

NO_AFFILIATION 을 보면, 해당 객체의 Null을 피하기 위한 하나의 수단으로 사용되고 어떻게 사용되는지 확인해보면, Employee 에서 아래와 같이 코딩합니다.

public class Employee {

        private PaymentClassification paymentClassification;
        private PaymentSchedule paymentSchedule;
        private PaymentMethod paymentMethod;
        private final int employeeId;
        private String name;
        private String address;
        private UnionAffiliation unionAffiliation = UnionAffiliation.NO_AFFILIATION;

        public Employee(int employeeId, String name, String address) {
                this.employeeId = employeeId;
                this.name = name;
                this.address = address;
        }
	...
    }

중간에 NO_AFFILIATION 을 살펴볼 수 있습니다. 기본 값을 NO_AFFILIATION 을 줌으로써, Null을 피합니다.


마무리

 디자인패턴으로 이해하는 PayrollSystem을 진행하고, 각 패턴을 이해하면서 아직까지는 어려운 부분이 나왔다고 생각하지는 않습니다. 책의 뒷편 너머로 점점더 난이도있는 이해를 요구하는 패턴이 나오고 있더라구요.

직접 코딩을 하면서 디자인패턴을 적용한 것이 아닌, 찬찬히 하나씩 따라가면서 패턴을 이해하고- 외우기 위한 노력을 했습니다. 각 패턴이 융합적으로 쓰인 부분이 인상깊었구요.

 

 

댓글