Loading...
Skip to Content

비즈니스 로직을 SQL에 관리하는 시스템과 도메인 클래스에서 관리하는 시스템의 차이점 Part1

 오늘 우리는 '비즈니스 로직을 SQL에 관리하는 시스템과 도메인 클래스에서 관리하는 시스템의 차이점'을 위해 회사 전직원의 급여계산을 위한 oracle stored procedure 를 작성해보도록 하겠습니다.

Oracle Stored Procedure를 작성하여 전직원의 급여 계산을 수행하는 과정은 다음과 같은 단계를 포함할 수 있습니다. 기본급여, 야근 수당, 무급 휴가 사용에 따른 공제, 그리고 세금 공제를 계산하는 것입니다. 각 직원별로 기본급여 테이블(employees), 근무 일지(work_logs), 휴가 일지(leave_records) 등을 참조해야 합니다. 아래 예시는 이러한 프로세스를 구현하는 방법을 보여줍니다. 실제 사용 시에는 테이블 구조와 비즈니스 요구 사항에 따라 조정이 필요할 수 있습니다.

CREATE OR REPLACE PROCEDURE calculate_payroll AS BEGIN FOR rec IN (SELECT e.employee_id, e.base_salary FROM employees e) LOOP -- 야근 수당 계산 DECLARE overtime_hours NUMBER; overtime_rate NUMBER := 1.5; -- 야근 수당 비율 overtime_pay NUMBER; BEGIN SELECT SUM(over_hours) INTO overtime_hours FROM work_logs WHERE employee_id = rec.employee_id AND work_date BETWEEN trunc(sysdate, 'MM') AND last_day(sysdate); -- 현재 월에 해당하는 기록만 선택 IF overtime_hours IS NULL THEN overtime_hours := 0; END IF; overtime_pay := overtime_hours * (rec.base_salary / 160) * overtime_rate; -- 160시간 기준 END; -- 무급 휴가 공제 계산 DECLARE unpaid_leave_days NUMBER; unpaid_deduction NUMBER; BEGIN SELECT SUM(leave_days) INTO unpaid_leave_days FROM leave_records WHERE employee_id = rec.employee_id AND leave_type = 'Unpaid' AND leave_date BETWEEN trunc(sysdate, 'MM') AND last_day(sysdate); -- 현재 월에 해당하는 기록만 선택 IF unpaid_leave_days IS NULL THEN unpaid_leave_days := 0; END IF; unpaid_deduction := (rec.base_salary / 20) * unpaid_leave_days; -- 월 기준 20일로 계산 END; -- 세금 공제 계산 (여기서는 간단한 예시로 세금 공제율을 사용) DECLARE tax_rate NUMBER := 0.1; -- 세금 비율 10% tax_deduction NUMBER; BEGIN tax_deduction := (rec.base_salary + overtime_pay - unpaid_deduction) * tax_rate; END; -- 최종 급여 업데이트 UPDATE employees SET final_salary = rec.base_salary + overtime_pay - unpaid_deduction - tax_deduction WHERE employee_id = rec.employee_id; END LOOP; COMMIT; END; /

이 Stored Procedure는 각 직원별로 기본급여를 가져온 후, 근무 일지와 휴가 일지를 참조하여 야근 수당과 무급 휴가 공제를 계산합니다. 그런 다음 세금을 공제하여 최종 급여를 계산하고 업데이트합니다. 이 프로시저는 각 월 말에 실행되어 해당 월의 급여를 계산하는 것을 목적으로 합니다. 실제 환경에서는 세금 계산 로직이 훨씬 더 복잡할 수 있으며, 이를 위해 추가적인 로직이 필요할 수 있습니다. 또한, 오류 처리 로직을 포함하여 로직의 견고성을 높일 수 있습니다.

 위의 로직을 개개직원 한명에 대한 계산 로직으로 stored procedure 의 파라미터로 직원 id 를 받아서 처리하는 것으로 변경하기

직원 한 명에 대한 급여 계산 로직으로 Stored Procedure를 변경하려면, 이전의 예제를 조금 수정하여 직원 ID를 매개변수로 받는 형태로 만들 수 있습니다. 아래의 예시 코드는 employee_id를 매개변수로 받아 해당 직원의 급여를 계산하는 프로시저를 보여줍니다.

CREATE OR REPLACE PROCEDURE calculate_individual_payroll(p_employee_id NUMBER) AS base_salary NUMBER; overtime_hours NUMBER; overtime_rate NUMBER := 1.5; -- 야근 수당 비율 overtime_pay NUMBER; unpaid_leave_days NUMBER; unpaid_deduction NUMBER; tax_rate NUMBER := 0.1; -- 세금 비율 10% tax_deduction NUMBER; final_salary NUMBER; BEGIN -- 기본급여 조회 SELECT base_salary INTO base_salary FROM employees WHERE employee_id = p_employee_id; -- 야근 수당 계산 SELECT NVL(SUM(over_hours), 0) INTO overtime_hours FROM work_logs WHERE employee_id = p_employee_id AND work_date BETWEEN trunc(sysdate, 'MM') AND last_day(sysdate); -- 현재 월에 해당하는 기록만 선택 overtime_pay := overtime_hours * (base_salary / 160) * overtime_rate; -- 160시간 기준 -- 무급 휴가 공제 계산 SELECT NVL(SUM(leave_days), 0) INTO unpaid_leave_days FROM leave_records WHERE employee_id = p_employee_id AND leave_type = 'Unpaid' AND leave_date BETWEEN trunc(sysdate, 'MM') AND last_day(sysdate); -- 현재 월에 해당하는 기록만 선택 unpaid_deduction := (base_salary / 20) * unpaid_leave_days; -- 월 기준 20일로 계산 -- 세금 공제 계산 tax_deduction := (base_salary + overtime_pay - unpaid_deduction) * tax_rate; -- 최종 급여 계산 final_salary := base_salary + overtime_pay - unpaid_deduction - tax_deduction; -- 최종 급여 업데이트 UPDATE employees SET final_salary = final_salary WHERE employee_id = p_employee_id; COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('Employee ID not found.'); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error calculating payroll for Employee ID: ' || TO_CHAR(p_employee_id)); END; /

이 Stored Procedure는 특정 직원 ID(p_employee_id)를 인자로 받아 그 직원의 기본급여, 야근 수당, 무급 휴가 공제, 세금을 계산한 후 최종 급여를 업데이트합니다. 해당 월에 대한 급여 계산을 수행하는 것으로, 실제 사용 시에는 직원 ID가 유효한지 등의 검증 절차와 오류 처리 로직을 포함할 필요가 있습니다. 또한, 실제 비즈니스 요구 사항에 맞게 세금 계산 방법이나 기타 계산 로직을 조정해야 할 수 있습니다.

위에서 제공한 Stored Procedure는 기본적으로 오라클 데이터베이스에서 직원 한 명의 급여를 계산하는 과정을 자동화합니다. 계산에는 기본 급여, 야근 수당, 무급 휴가에 따른 공제, 그리고 세금 공제가 포함됩니다. 각 계산 단계는 데이터베이스 내의 관련 테이블에서 필요한 정보를 조회하여 진행됩니다.

실행 방법:

Stored Procedure를 데이터베이스에 생성한 후, 아래와 같이 실행할 수 있습니다.

BEGIN calculate_individual_payroll(p_employee_id => 123); -- 123은 예시 직원 ID입니다. END; /

이 때, p_employee_id는 계산하고자 하는 직원의 ID를 매개변수로 받습니다. 이 매개변수에 해당하는 직원의 급여 정보를 기반으로 계산이 수행됩니다.

중요 사항:

  1. 데이터 정확성: Stored Procedure를 운영 환경에서 사용하기 전에, 사용하려는 데이터베이스의 스키마와 실제 데이터를 정확히 반영하는지 확인해야 합니다. 예를 들어, employees, work_logs, leave_records 테이블의 구조와 필드가 예제 코드와 일치하는지 검토해야 합니다.
  2. 예외 처리: Stored Procedure에서는 NO_DATA_FOUND와 OTHERS를 포함한 기본적인 예외 처리를 제공합니다. 하지만 실제 운영 환경에서는 더 다양한 예외 상황에 대응할 수 있도록 예외 처리 로직을 확장할 필요가 있습니다.
  3. 성능 최적화: 대규모 데이터셋을 처리할 때는 성능 문제가 발생할 수 있습니다. 필요에 따라 인덱스 추가, 쿼리 최적화, 또는 배치 처리 등의 방법을 고려해야 할 수 있습니다.
  4. 보안 고려: 직원의 급여 정보와 같은 민감한 데이터를 처리할 때는 데이터베이스와 애플리케이션 수준에서 적절한 보안 조치를 취하는 것이 중요합니다. 사용자 권한 관리, 데이터 암호화, SQL 인젝션 방지 기법 등을 적용해야 합니다.
  5. 테스트: 실제 데이터에 적용하기 전에 테스트 환경에서 충분한 테스트를 수행하여, 예상대로 정확히 동작하는지 확인해야 합니다.

이 프로시저는 특정 직원의 급여 계산을 자동화하는 강력한 도구가 될 수 있지만, 실제 환경에서는 복잡한 비즈니스 규칙, 다양한 예외 사항, 그리고 성능과 보안 요구사항을 충족시키기 위해 추가적인 개발과 테스트가 필요합니다.

 위의 급여 계산을 도메인 클래스를 기반한 자바 코드로 변환해보기

급여 계산 로직을 Java 코드로 변환할 때, 도메인 클래스를 기반으로 구현하는 것이 좋습니다. 이를 위해 직원(Employee), 근무 일지(WorkLog), 휴가 기록(LeaveRecord) 등에 대한 도메인 클래스를 정의하고, 급여 계산을 담당하는 서비스 클래스(PayrollService)를 만들어 볼 수 있습니다.

아래의 예제 코드는 간단한 도메인 클래스와 급여 계산 로직을 구현한 것입니다. 실제 환경에서는 데이터베이스 연동을 위해 JPA, JDBC, MyBatis 등의 기술을 사용할 수 있으며, 여기서는 코드의 이해를 돕기 위해 간략화된 형태로 구현하였습니다.

도메인 클래스 정의

public class Employee { private Long id; private Double baseSalary; private Double finalSalary; // 계산된 최종 급여를 저장 // Constructors, Getters and Setters } public class WorkLog { private Long employeeId; private Double overHours; // 해당 월의 야근 시간 // Constructors, Getters and Setters } public class LeaveRecord { private Long employeeId; private String leaveType; // "Paid" 또는 "Unpaid" private Double leaveDays; // 해당 월의 휴가 일수 // Constructors, Getters and Setters }

급여 계산 서비스 클래스

import java.util.List; public class PayrollService { public void calculatePayrollForEmployee(Long employeeId) { Employee employee = findEmployeeById(employeeId); List<WorkLog> workLogs = findWorkLogsForEmployee(employeeId); List<LeaveRecord> leaveRecords = findLeaveRecordsForEmployee(employeeId); double baseSalary = employee.getBaseSalary(); double overtimePay = calculateOvertimePay(workLogs, baseSalary); double unpaidDeduction = calculateUnpaidDeduction(leaveRecords, baseSalary); double taxDeduction = calculateTaxDeduction(baseSalary + overtimePay - unpaidDeduction); double finalSalary = baseSalary + overtimePay - unpaidDeduction - taxDeduction; employee.setFinalSalary(finalSalary); // Update employee's final salary in database (not shown here) } private double calculateOvertimePay(List<WorkLog> workLogs, double baseSalary) { double overtimeHours = workLogs.stream().mapToDouble(WorkLog::getOverHours).sum(); return overtimeHours * (baseSalary / 160) * 1.5; // 예시로 1.5배 야근 수당 적용 } private double calculateUnpaidDeduction(List<LeaveRecord> leaveRecords, double baseSalary) { double unpaidLeaveDays = leaveRecords.stream() .filter(lr -> "Unpaid".equals(lr.getLeaveType())) .mapToDouble(LeaveRecord::getLeaveDays) .sum(); return (baseSalary / 20) * unpaidLeaveDays; // 월 기준 20일로 계산 } private double calculateTaxDeduction(double taxableIncome) { return taxableIncome * 0.1; // 예시로 10% 세율 적용 } // These methods would interact with the database to retrieve the relevant data private Employee findEmployeeById(Long employeeId) { /* ... */ } private List<WorkLog> findWorkLogsForEmployee(Long employeeId) { /* ... */ } private List<LeaveRecord> findLeaveRecordsForEmployee(Long employeeId) { /* ... */ } }

이 코드 예제에서는 각 클래스의 필드, 생성자, getter, setter 메서드는 구현을 생략하였으며, 실제로 사용하기 위해서는 이들을 적절히 구현해야 합니다. 또한, findEmployeeById, findWorkLogsForEmployee, findLeaveRecordsForEmployee 같은 데이터베이스 접근 메서드는 실제 데이터베이스 연동 로직으로 구현되어야 합니다.

 위 로직을 Employee 엔티티의 오퍼레이션에 포함 시켜보기

직원(Employee) 엔티티 내에 급여 계산 로직을 포함시키는 것은 도메인 주도 설계(DDD)에서 종종 볼 수 있는 패턴입니다. 이 접근법은 엔티티가 자신의 데이터를 사용하여 비즈니스 로직을 수행할 수 있게 하여 캡슐화를 강화합니다. 하지만, 이 경우 Employee 클래스가 근무 일지(WorkLog)와 휴가 기록(LeaveRecord)에 대한 참조도 가져야 하며, 이는 엔티티 간의 결합도를 증가시킬 수 있습니다. 따라서 신중하게 고려해야 합니다.

아래는 Employee 엔티티 내에 급여 계산 로직을 포함시킨 예제입니다. 이 예제에서는 Employee가 자신의 근무 일지와 휴가 기록 리스트를 직접 관리한다고 가정합니다.

import java.util.List; public class Employee { private Long id; private Double baseSalary; private Double finalSalary; private List<WorkLog> workLogs; private List<LeaveRecord> leaveRecords; // Constructors, getters and setters 생략 // 급여 계산 public void calculatePayroll() { double overtimePay = calculateOvertimePay(); double unpaidDeduction = calculateUnpaidDeduction(); double taxDeduction = calculateTaxDeduction(baseSalary + overtimePay - unpaidDeduction); finalSalary = baseSalary + overtimePay - unpaidDeduction - taxDeduction; } private double calculateOvertimePay() { double overtimeHours = workLogs.stream().mapToDouble(WorkLog::getOverHours).sum(); return overtimeHours * (baseSalary / 160) * 1.5; // 예시로 1.5배 야근 수당 적용 } private double calculateUnpaidDeduction() { double unpaidLeaveDays = leaveRecords.stream() .filter(lr -> "Unpaid".equals(lr.getLeaveType())) .mapToDouble(LeaveRecord::getLeaveDays) .sum(); return (baseSalary / 20) * unpaidLeaveDays; // 월 기준 20일로 계산 } private double calculateTaxDeduction(double taxableIncome) { return taxableIncome * 0.1; // 예시로 10% 세율 적용 } // workLogs와 leaveRecords에 대한 접근자 메소드 생략 }

이 예제는 각 Employee 인스턴스가 자신의 근무 일지(workLogs)와 휴가 기록(leaveRecords) 리스트를 관리하고, 이 정보를 사용하여 자신의 급여를 계산할 수 있게 합니다. 이 접근법의 장점은 비즈니스 로직이 데이터와 밀접하게 연결되어 있어 캡슐화가 강화된다는 것입니다. 하지만, 모델이 복잡해지고 엔티티 간의 결합도가 증가하는 단점도 있습니다. 또한, 근무 일지와 휴가 기록 데이터의 접근 및 관리 방법은 실제 프로젝트의 아키텍처와 기술 스택에 따라 달라질 수 있습니다.

 전직원에 대한 계산로직을 돌리기 위한 서비스 하나를 만들고, 직원 몇 경우의 단위테스트도 작성해보기

이 접근 방식을 사용하여 전체 직원에 대한 급여 계산 로직을 수행하는 서비스를 만들고, 해당 서비스에 대한 단위 테스트 몇 가지를 작성하는 방법을 살펴보겠습니다. 여기서는 서비스 클래스 이름을 PayrollCalculatorService로 하고, JUnit을 사용하여 단위 테스트를 작성하겠습니다.

전직원 급여 계산 서비스

import java.util.List; public class PayrollCalculatorService { private EmployeeRepository employeeRepository; // 데이터 접근을 위한 리포지토리 // 의존성 주입을 위한 생성자 또는 세터 (생략) public void calculatePayrollForAllEmployees() { List<Employee> employees = employeeRepository.findAll(); for (Employee employee : employees) { employee.calculatePayroll(); // Employee 클래스에 정의된 메소드 호출 employeeRepository.save(employee); // 계산된 급여를 저장 } } }

이 서비스 클래스는 모든 직원에 대해 급여 계산을 수행합니다. EmployeeRepository는 데이터베이스에서 직원 정보를 조회하고, 계산된 급여 정보를 저장하는 데 사용됩니다. 실제 구현에서는 Spring Data JPA, MyBatis 등을 사용하여 구현할 수 있습니다.

단위 테스트 작성

JUnit을 사용하여 PayrollCalculatorService의 단위 테스트를 작성합니다. 여기서는 Mockito를 사용하여 의존성을 모의로 처리하며, 직원 급여 계산 로직의 정확성을 검증합니다.

import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.List; public class PayrollCalculatorServiceTest { @Test public void testCalculatePayrollForAllEmployees() { // Given EmployeeRepository mockRepository = Mockito.mock(EmployeeRepository.class); PayrollCalculatorService service = new PayrollCalculatorService(); service.setEmployeeRepository(mockRepository); // 의존성 주입 Employee employee1 = new Employee(1L, 3000.0); // 예제 직원 Employee employee2 = new Employee(2L, 4000.0); List<Employee> employees = Arrays.asList(employee1, employee2); when(mockRepository.findAll()).thenReturn(employees); // When service.calculatePayrollForAllEmployees(); // Then for (Employee employee : employees) { verify(mockRepository).save(employee); // 각 직원에 대해 save가 호출되었는지 확인 } } }

이 테스트는 PayrollCalculatorService가 EmployeeRepository를 사용하여 모든 직원의 급여를 계산하고, 결과를 저장하는지 검증합니다. 실제 Employee 인스턴스의 calculatePayroll 메소드가 올바르게 작동하는지는 Employee 클래스 자체의 단위 테스트를 통해 검증해야 합니다. 위의 코드에서는 간소화를 위해 Employee 클래스의 생성자와 calculatePayroll 메소드 구현을 생략하였습니다.

이 예제들은 기본적인 구조를 제공합니다. 실제 애플리케이션에서는 비즈니스 요구 사항, 데이터베이스 설계, 프로젝트에서 사용하는 기술 스택 등에 따라 구현이 달라질 수 있습니다.

 결론

비즈니스 로직을 처리하는 두 가지 접근 방식, 즉 SQL 내에 로직을 포함하는 Stored Procedure를 사용하는 Transaction Script 접근과 Java에서 도메인 클래스를 사용하는 접근 방식에 대해 비교해 보겠습니다. 여러 관점에서의 비교를 통해 각 접근 방식의 장단점을 이해하고, 상황에 따른 적절한 선택을 돕는 비교표를 제시합니다.

비교 항목 Stored Procedure (Transaction Script) Java (Domain Model) 비고
비즈니스 로직의 인캡슐레이션 낮음. 로직이 데이터베이스와 밀접하게 결합됨. 높음. 비즈니스 로직이 객체 내에 캡슐화됨. 도메인 모델은 비즈니스 로직을 객체 내에 캡슐화하여 높은 수준의 추상화와 응집력을 제공합니다.
가독성 낮을 수 있음. 복잡한 SQL과 PL/SQL은 읽기 어려울 수 있음. 높음. 자바와 같은 고급 언어는 가독성이 좋고 이해하기 쉬움. 도메인 모델은 비즈니스 로직을 명확하게 표현하는 경향이 있으며, 코드의 의도를 이해하기 쉽습니다.
유지보수성 중간에서 낮음. 로직 변경 시 테스트와 배포가 어려울 수 있음. 높음. 유닛 테스트와 CI/CD를 통한 쉬운 유지보수. 자바와 같은 언어는 리팩토링, 유닛 테스트, 버전 관리가 용이하며, 변경 사항의 배포가 더 쉽습니다.
데이터베이스 제품 변경 시 영향도 높음. 특정 데이터베이스와 밀접하게 결합됨. 낮음. JDBC, JPA와 같은 추상화 레이어를 통해 데이터베이스 독립성 제공. 도메인 모델 방식은 데이터베이스 교체 시 애플리케이션 로직에 미치는 영향을 최소화합니다.
테스트 가능성 낮음. 데이터베이스가 필요하며, 격리된 테스트가 어려움. 높음. 유닛 테스트와 모의 객체(Mock)를 사용하여 격리된 테스트 가능. 도메인 모델은 격리된 유닛 테스트를 통해 더 빠르고 효과적인 테스트가 가능합니다.

상황에 따른 적절한 선택

Stored Procedure (Transaction Script) 적합한 경우:

  • 로직이 데이터 처리에 크게 의존하는 간단한 CRUD 작업일 때.
  • 데이터베이스 성능을 최적화하고, 모든 로직을 데이터베이스 서버에서 처리해야 할 때.
  • 데이터베이스 전문가가 주도하는 프로젝트에서 데이터베이스 내부적인 처리를 선호할 때.

Java (Domain Model) 적합한 경우:

  • 복잡한 비즈니스 로직이 있고, 객체 지향적 접근 방식을 통한 모델링이 필요할 때.
  • 애플리케이션의 유지보수성과 확장성이 중요한 프로젝트에서.
  • 데이터베이스 독립성을 유지하면서 다양한 데이터 소스를 활용하고자 할 때.
  • 팀이 객체 지향 프로그래밍에 익숙하고, 자바(JVM) 생태계 내의 도구와 라이브러리를 활용하고자 할 때.

각 접근 방식은 서로 다른 장단점을 가지고 있으며, 프로젝트의 요구 사항, 팀의 기술 스택 및 전문성, 애플리케이션의 복잡성 등 여러 요소를 고려하여 결정해야 합니다. 예를 들어, 비즈니스 로직이 매우 복잡하고 객체 지향적 설계의 이점을 최대한 활용하고자 한다면 Java를 사용한 도메인 모델 접근 방식이 더 적합할 수 있습니다. 반면, 데이터 처리와 관련된 로직이 강조되고, 특정 데이터베이스 기능(예: 복잡한 SQL, 트랜잭션 관리, 최적화 등)을 극대화해야 하는 경우에는 Stored Procedure를 사용하는 것이 더 효율적일 수 있습니다.

또한, 테스트 가능성의 측면에서 볼 때, Java 기반의 접근 방식은 유닛 테스트와 통합 테스트를 통해 더 견고한 테스트 커버리지를 구축할 수 있도록 해줍니다. 이는 애플리케이션의 안정성을 높이고, 오류를 조기에 발견하여 수정하는 데 도움이 됩니다.

최종적으로, 프로젝트의 성공은 선택한 기술뿐만 아니라, 그 기술을 어떻게 활용하고, 팀이 얼마나 잘 협업하고, 프로젝트 관리를 어떻게 진행하느냐에도 크게 좌우됩니다. 따라서, 각 방식의 장단점을 충분히 이해하고, 프로젝트의 구체적인 상황에 맞는 최적의 접근 방식을 선택하는 것이 중요합니다.