이 글은 조영호 님의 저서 "오브젝트 Objects"를 읽고 적용한 후기입니다.
🐳 책임 주도 설계 적용해 보기
나는 항상 학습을 할 때 이론처럼 공부만 하기 보다는 실전에 적용해 보려고 노력을 하는 편이다.
그렇게 공부하는 것이 더 빠르게 학습할 수 있는 방법이라고 생각했기 때문이다.
마침 회사에서 근태관리 서비스를 개발할 때 DB설계와 도메인 모델 설계를 맡게 되어 책임 주도 설계를 적용해 보기로 했다.
이 글에서는 NodeJS, ExpressJS를 활용해서 책임 주도 설계를 적용해 실제로 근태 관리 시스템을 개발해 볼 것이다.
( 비판은 언제나 달게 수용하겠습니다 ! 재밌게 봐주시기 바랍니다 ! )
🐳 책임 주도 설계 원칙과 방법
도서 "오브젝트" 134p 에 이런 말이 나온다.
데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다.
1. 데이터보다 행동을 먼저 결정하라
2. 협력이라는 문맥 안에서 책임을 결정하라
기존의 데이터 중심의 설계에서는 DB를 어떻게 설계하고.
데이터 들은 어떤 관계를 가지는지를 먼저 설계했다면
책임 주도 설계는 행동을 결정하고 그 행동을 책임질 객체를 설정하는 과정으로 진행된다는 것이다.
그렇다면 책임 주도 설계는 어떠한 방법으로 진행될까 ?
이 또한 당연히 도서 "오브젝트"에 나와있다 !
( 이쯤되면 이 글을 읽는 당신도 한 번 읽어보자 ! )
책임 주도 설계의 방법은 아래와 같다. p84
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 도는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
위의 방법을 따라가면서 책임 주도 설계를 적용해보자
(이제 진짜 시작합니다 ! )
🐳 근태관리 도메인 개념
먼저 들어가기에 앞서서 근태관리에 필요한 도메인 개념들을 정리해 보자.
사원
가정 먼저 근태관리의 대상인 사원이다.
이 사원은 근무기록을 생성한다.
근무기록
사원들의 근무기록이다.
근무기록의 종류는 일반 출퇴근, 오후반차, 오전반차, 출장, 휴가가 존재한다고 가정하자.
하루 중에 근무 기록은 여러가지 종류를 가질 수 있다.
오전에 출장을 간 후, 출근한 후에 오후 반차를 쓰고 퇴근할 수 있는 것이다 !
이런 케이스에는 하루에 근무기록이 세 건이 생성될 수 있다. 출장,출근/퇴근,오후반차
( 모든 근무기록은 시작시간과 끝 시간이 있기 때문에 출근과 퇴근은 시작 시간과 끝 시간으로 생각해 하나의 데이터로 보았습니다. )
규칙
근무기록을 바탕으로 현재의 근무 상태를 판단하는 규칙이다.
규칙이라는 추상적인 개념을 하나의 객체로 생각했다.
규칙은 오전 규칙과 오후 규칙이 다르다.
가령 오전반차를 사용한 사람이 있다고 생각해 보자.
오전 규칙으로 이 사람의 근무상태를 파악하면 오전반차가 될 것이다.
하지만 오후 규칙으로 이 사람의 근무상태를 파악하면 아직 출근하지 않았다는 전제 하에 지각이 될 것이다.
이처럼 규칙에 따라 근무상태가 달라지게 된다.
클라이언트
요청자이다.
요청자는 현재 시각의 사원들의 근무상태 ( 출근 / 퇴근 / 오전반차 / 오후반차 / 휴가 / 결근 ) 의 상태를 알고 싶다.
🐳 시스템 책임 파악 및 분할
시스템이 사용자에게 제공하고자 하는 기능은 무엇일까 ?
클리이언트가 원하는 것은 근무태도를 관리하는 것이다.
시스템 책임은 근태 관리가 될 것이다.
하지만 이 개념은 너무 방대하므로 시스템 책임을 분할해 보자.
근무태도 관리에는 "현재 사원들의 근무상태를 구하라" 라는 더 작은 시스템 책임이 있을 것이다.
이번 글에서는 이 작은 시스템 책임인 "현재 사원들의 근무상태를 구하라" 라는 책임을 구현해 보자.
그렇다면 클라이언트가 전달할 메시지는 무엇일까?
"근무상태를 구하라"가 될 것이다.
책임 주도 설계는 위와 같은 메시지로부터 시작한다.
🐳 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당하기
"근무상태를 구하라"라는 책임을 수행할 수 있는 적절한 객체 또는 역할은 무엇일까 ?
근무상태를 구하기 위해서는 무엇이 필요할까 ?
이 메시지를 어떤 객체 또는 역할이 받을지 생각해 보자.
근무상태에 관해서 가장 잘 아는 객체가 누구인지 생각해 보면 될 것이다.
위의 도메인 개념 중 규칙일 것이다.
규칙은 근무기록을 바탕으로 근무상태를 결정하기 때문이다.
이 규칙은 여러가지 종류가 (오전규칙 / 오후규칙 / 업무시간 외) 있기 때문에 하나의 객체가 아닌 동일한 메시지를 수용할 수 있는 하나의 역할의 개념으로 생각해야 할 것이다.
그렇다면 근무상태를 구하라 라는 메시지를 수신하는 측을 규칙이라고 가정을 해보자.
그렇다면 규칙은 어떤 정보가 필요할까 ?
규칙 자체는 근무기록을 입력 받았을 때 규칙을 기반으로 근무상태만을 구하므로 근무 기록을 알아야 할 것이다.
그렇다면 다음 메시지는 근무기록을 가지고 있는 객체에게 "근무기록을 구하라"라는 메시지를 전해야 할 것이다.
위에서 찾았던 것과 같은 방법으로 "근무기록을 구하라"라는 메시지를 수신할 객체 또는 역할을 찾자.
근무기록에 대해 알고 있는 객체 또는 역할은 무엇일까?
바로 사원일 것이다.
사원은 근무기록을 생성하고 근무기록을 소유하기 때문이다.
사원에게 "근무기록을 구하라"라는 메시지를 수신하고 응답할 책임을 주자.
그러면 사원은 가지고 있는 근무기록을 전달할 것이다.
🐳 규칙 인터페이스로 구현하기
규칙은 여러가지 규칙이 존재할 수 있다.
오전반차를 사용한 사람의 근무상태를 예시를 들어보자.
오전반차를 사용한 사람의 근무상태는 "오전반차" 상태일 것이다.
이 사람이 출근을 하지 않은 상태로 오후 업무 시간이 되었다고 생각해 보자.
이 사람의 오후 근무상태는 지각 또는 결근이 되야할 것이다.
이처럼 이 사람의 근무기록은 변경되지 않았지만 어떤 규칙으로 이 사람의 근무기록을 평가하느냐에 따라서
근무상태가 달라질 수 있다.
하지만 규칙들은 공통적으로 "근무상태를 구하라" 라는 메시지를 수신해야할 책임이 있다.
오전 규칙, 오후 규칙, 근무시간 외 규칙 모두 "근무상태를 구하라"라는 책임이 있다.
이 규칙들은 동일한 메시지를 수신한다는 차원에서 같은 역할을 가진다고 볼 수 있다.
이 규칙이라는 역할을 인터페이스를 통해 구현하자.
이 규칙을 실체화 하는 실제 클래스 들이 오전 규칙, 오후 규칙, 근무시간 외 규칙이 될 것이다.
그리고 이렇게 인터페이스를 이용해서 구현하게 변화에 대응하기 용이해진다.
회사 정책이 변해 시간 단위로 연차를 사용할 수 있다고 가정해보자.
그렇다면 근무상태를 구하기 위해서 우리가 해주어야 할 것은 규칙 Interface를 구현하는 새로운 규칙만 생성해 주면 되는 것이다 !
그렇다면 이제 실제 코드로 구현해 보자.
🐳 NodeJS, ExpressJS로 코드 구현하기
Javascript 자체가 클래스를 사용하기에 편한 언어는 아니지만
회사에서 사용하는 언어가 Javascript이고 회사에 적용해야 하는 개념들이기에 NodeJS 와 ExpressJS를 활용해 코드를 구현해 보았다.
Rule 구현하기
Javascript는 공식적으로 interface를 지원하지 않기 때문에 class로 interface를 구현해 보자.
class IRule {
determineWorkStatus(histories) {
throw new Error()
}
}
module.exports = IRule;
일반적인 Class와 다를게 없지만 근무기록을 입력 받아 WorkStatus를 구하는 메소드를 구현하지 않으면 Error를 발생시키도록 구현했다.
Interface를 지원하는 다른 언어라면 Interface를 implements 해야 하지만
우리는 Class를 이용해서 구현했으므로 위의 IRule을 상속 받아서 MoringRule, AfternoonRule, NonWorkingHourRule을 구현해 보자.
class MorningRule extends IRule {
determineWorkStatus(work_histories) {
// 오전 근무 상태 결정 로직 구현
// 로직 구현...
return "Morning Working";
}
}
module.exports = MorningRule;
class AfternoonRule extends IRule{
determineWorkStatus(work_histories) {
// 오후 근무 상태 결정 로직 구현
// 로직 구현...
return "Afternoon Working";
}
}
module.exports = AfternoonRule;
class NonWorkingHoursRule extends IRule {
determineWorkStatus(work_histories) {
// 근무 외 시간 근무 상태 결정 로직 구현
// 로직 구현...
return "Non Working";
}
}
module.exports = NonWorkingHoursRule;
실제 비즈니스 로직은 없지만 위의 처럼 IRule을 extends 해 규칙들을 구현하면 모두 다 동일한 메시지를 수신할 수 있게 된다.
RuleFactory 구현하기
이제는 규칙을 만들어줄 팩토리를 구현해 보자.
// rule.factory.js
const MorningRule = require('./MorningRule');
const AfternoonRule = require('./AfternoonRule');
const NonWorkingHourRule = require('./nonWorkingHourRule');
const WORK_TIME= require('./workTime.enum')
class RuleFactory {
static getRuleInstance() {
const hours = new Date().getHours() % 24 ;
if (WORK_TIME.BEGIN <= hours && hours < WORK_TIME.LUNCH) {
return new MorningRule();
} else if (WORK_TIME.LUNCH <= hours && hours < WORK_TIME.END) {
return new AfternoonRule();
} else {
return new NonWorkingHourRule();
}
}
}
module.exports = RuleFactory;
오전 근무 시간인 업무 시작 시간부터 점심시간 전 까지는 MorningRule 인스턴스를 리턴한다.
오후 근무 시간인 점심시간부터 퇴근시간 전 까지는 AfternoonRule 인스턴스를 리턴한다.
그 외의 업무 외 시간은 NonWorkingHourRule 인스턴스를 리턴한다.
번외
Javascript 에서는 enum을 지원하지 않기 때문에 객체를 활용해서 WORK_TIME enum을 구현해서 사용했다.
공식적인 enum은 아니지만 매직넘버를 피할 수 있고 코드의 가독성을 높일 수 있다.
(블로그의 다른 글을 참고해 주세요. [Node.js] - [ExpressJS] Javascript로 Enum 사용하기 - 오픈카톡방 공유 후기 )
Employee Class 구현하기
class Employee {
employeeId;
employeeName;
roleName;
histories;
constructor(employeeId,employeeName,roleName) {
this.employeeId = employeeId;
this.employeeName = employeeName;
this.roleName = roleName;
}
setHistories(histories) {
this.histories = histories;
}
getHistories() {
return this.histories
}
}
Employee Class를 구현해보자
Employee는 근무기록을 가질 수 있기 때문에 Employee의 기본 생성자를 제외한 histories를 설정할 수 있는 set 메소드와 조회하는 get method를 구현했다.
비즈니스 로직 구현
const rule = RuleFactory.getRuleInstance();
const employeeWithWorkHistories = await work_history.todayList(req)
const employeeWorkStatus = employeeWithWorkHistories.list.map(employee => {
return {
...employee,
workStatus:rule.determineWorkStatus(employee.getHistories())
}
})
여지껏 Class 들을 구현했다면 이번에는 실제로 위에서 작성한 Class 들을 사용하는 코드들을 살펴보자.
1. Rule 생성
먼저 RuleFactory를 이용해 rule을 생성해 주었다.
이 때 내가 생성한 Rule은 오전규칙인지, 오후규칙인지, 근무시간 외 규칙인지 알 수 없다.
또한 우리는 생성된 규칙이 내부에서 어떤 로직으로 동작하는지 알 수 없다.
하지만 위의 세 가지 규칙들은 모두 동일하게 determinWorkStatus 라는 동일한 메시지를 수신할 책임이 있다 !
이처럼 코드가 간결해지는 것을 볼 수 있다.
2. employeeWithWorkHistories 조회
work_history 는 실제 DB와 접속하여 Data를 조회하는 service layer이다.
여기서 가지고 오는 값은 우리가 위에서 작성한 Employee Class에 setHistories를 통하여 Employee에 histories를 주입한 결과값을 리턴한다.
이 때 employeeWithWorkHistories.list 에는 조회한 데이터의 결과를 employeeWithWorkHistories.count에는 총 개수를 리턴한다.
3. employeeWorkStatus
대망의 사원의 근무상태를 생성하는 코드이다.
const employeeWorkStatus = employeeWithWorkHistories.list.map(employee => {
return {
...employee,
workStatus:rule.determineWorkStatus(employee.getHistories())
}
})
코드를 살펴보자 !
employeeWithWorkHistories.list 를 map을 통해 순회하며 새로운 배열을 생성한다.
workStatus 생성 로직을 보자
rule 객체에게 determineWorkStatus 메시지를 전송한다.
이 때 주입해야 하는 histories는 employee에게 있으므로
employee에게 getHistories 메시지를 전송한다.
🐳 후기
배운 것을 회사 프로젝트에 적용해 보기 위해서 시작했는데 생각보다 정~~말 많이 모자르구나라는 것을 느꼈다.
(글 쓰는 데만 5시간 정도가 소요됐다.. )
하지만 첫 술에 배부르랴
책임 주도 설계를 한 번 도전해 봤다는 것에 의미가 있는 것 같다 !
이렇게 저렇게 도전해 보고, 다른 사람들의 피드백도 받아보고 하다 보면 많이 늘지 않겠습니까 ?!
모자른 글이지만 끝까지 읽어 주셔서 정말 감사하고 모자른 부분 투성이일텐데 애정 담긴 비판 남겨주시면 달게 받고 적용하도록 하겠습니다 !
'독서' 카테고리의 다른 글
[오브젝트] 2. 캡슐화 (1) | 2023.11.19 |
---|---|
[오브젝트] 1. 객체,설계 (1) | 2023.11.16 |
[함께 자라기] - 자라기 (0) | 2023.09.27 |