800===Dev Docs and License/이론 문서

객체지향 심화 학습 1편 SRP : SOLID 원칙 완전정복 🎯

블로글러 2024. 11. 3. 21:03

오늘은 SOLID 원칙을 하나씩 자세히 살펴보면서, 실제 코드로 어떻게 적용하는지 알아볼게요!

1. 단일 책임 원칙 (SRP) 이해하기 💡

핵심: "한 클래스는 단 하나의 변경 이유만 가져야 한다"

Bad Case: SRP 위반 사례

// ❌ 이렇게 하면 안돼요!
public class Employee {
    public void calculatePay() {     // 급여 계산
        // 복잡한 급여 계산 로직
    }

    public void saveEmployee() {     // DB 저장
        // DB 저장 로직
    }

    public void generateReport() {   // 리포트 생성
        // 리포트 생성 로직
    }
}

이게 왜 문제일까요? 🤔

  1. 급여 정책이 바뀌면 수정 필요
  2. DB가 변경되면 수정 필요
  3. 리포트 형식이 바뀌면 수정 필요

-> 세 가지 서로 다른 이유로 클래스가 변경될 수 있어요!

Good Case: SRP 적용 사례

// ✅ 이렇게 분리하면 좋아요!
public class Employee {
    private String id;
    private String name;
    private Money salary;

    // 직원의 기본 정보만 관리
    public void updateInfo(EmployeeInfo info) {
        this.name = info.getName();
        // ... 직원 정보 관련 로직만!
    }
}

// 급여 계산만 담당
@Service
public class PayCalculator {
    private final PayPolicy payPolicy;

    public Money calculatePay(Employee employee) {
        return payPolicy.calculate(employee);
    }
}

// DB 저장만 담당
@Repository
public class EmployeeRepository {
    public void save(Employee employee) {
        // DB 저장 로직
    }
}

// 리포트 생성만 담당
@Service
public class EmployeeReportGenerator {
    public Report generateReport(Employee employee) {
        // 리포트 생성 로직
        return new Report(employee);
    }
}

장점이 뭘까요? 🌟

  1. 코드가 더 명확해져요
    • 각 클래스가 하는 일이 분명함
    • 클래스 이름만 봐도 역할을 알 수 있음
  2. 유지보수가 쉬워져요
    • 급여 정책이 바뀌면 PayCalculator만 수정
    • DB가 바뀌면 EmployeeRepository만 수정
    • 리포트 형식이 바뀌면 EmployeeReportGenerator만 수정
  3. 테스트가 쉬워져요
  4. @Test void calculatePay_정상급여계산() { // Given Employee employee = new Employee("John", Money.wons(3000000)); PayCalculator calculator = new PayCalculator(new RegularPayPolicy()); // When Money salary = calculator.calculatePay(employee); // Then assertThat(salary).isEqualTo(Money.wons(3000000)); }

실무 적용 예시: 주문 시스템

// 주문 도메인 (핵심 비즈니스 로직만 담당)
public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private OrderStatus status;

    public void place() {
        validateOrder();
        this.status = OrderStatus.PLACED;
    }

    private void validateOrder() {
        if (items.isEmpty()) {
            throw new EmptyOrderException();
        }
    }
}

// 주문 처리 (트랜잭션 처리 담당)
@Service
@Transactional
public class OrderProcessor {
    private final OrderRepository repository;
    private final OrderEventPublisher eventPublisher;

    public OrderId processOrder(Order order) {
        order.place();
        OrderId orderId = repository.save(order);
        eventPublisher.publishOrderPlaced(order);
        return orderId;
    }
}

// 주문 알림 (알림 발송 담당)
@Service
public class OrderNotifier {
    private final EmailSender emailSender;
    private final SmsSender smsSender;

    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // 이메일 발송
        emailSender.sendOrderConfirmation(event.getOrder());

        // SMS 발송
        smsSender.sendOrderNotification(event.getOrder());
    }
}

실전 적용 시 주의사항 ⚠️

1. 과도한 분리는 피하기

// ❌ 너무 잘게 쪼개면 오히려 복잡해질 수 있어요
public class EmployeeNameUpdater {
    public void updateName(Employee employee, String name) {
        employee.setName(name);
    }
}

public class EmployeeSalaryUpdater {
    public void updateSalary(Employee employee, Money salary) {
        employee.setSalary(salary);
    }
}

// ✅ 관련된 책임은 함께 유지하는 게 좋아요
public class EmployeeInfoManager {
    public void updateEmployeeInfo(Employee employee, 
                                 EmployeeInfo newInfo) {
        employee.setName(newInfo.getName());
        employee.setSalary(newInfo.getSalary());
        // 직원 정보 업데이트와 관련된 모든 로직
    }
}

2. 문맥에 따라 유연하게 적용하기

// 작은 프로젝트에서는 이 정도도 괜찮아요
public class SimpleEmployeeService {
    public void updateEmployee(Employee employee, 
                             EmployeeInfo newInfo) {
        // 간단한 업데이트 로직
        employee.update(newInfo);

        // 간단한 알림
        sendUpdateNotification(employee);
    }
}

// 규모가 커지면 이렇게 분리
public class EnterpriseEmployeeService {
    private final EmployeeValidator validator;
    private final EmployeeRepository repository;
    private final EmployeeEventPublisher eventPublisher;

    public void updateEmployee(Employee employee, 
                             EmployeeInfo newInfo) {
        // 검증
        validator.validate(newInfo);

        // 업데이트
        employee.update(newInfo);
        repository.save(employee);

        // 이벤트 발행
        eventPublisher.publishEmployeeUpdated(employee);
    }
}

단일 책임 원칙의 장점 정리 🎁

  1. 코드 가독성 향상
    • 각 클래스의 책임이 명확해져서 코드를 이해하기 쉬워요
  2. 유지보수성 향상
    • 변경이 필요할 때 관련 클래스만 수정하면 돼요
    • 다른 부분에 영향을 주지 않아요
  3. 재사용성 향상
    • 작은 단위로 분리되어 있어 필요한 기능만 가져다 쓸 수 있어요
  4. 테스트 용이성
    • 각 책임별로 독립적인 테스트가 가능해요
// 테스트가 쉬워져요
@Test
void 직원정보_업데이트() {
    // Given
    EmployeeInfoManager manager = new EmployeeInfoManager();
    Employee employee = new Employee("John", Money.wons(3000000));
    EmployeeInfo newInfo = new EmployeeInfo("John Kim", Money.wons(3500000));

    // When
    manager.updateEmployeeInfo(employee, newInfo);

    // Then
    assertThat(employee.getName()).isEqualTo("John Kim");
    assertThat(employee.getSalary()).isEqualTo(Money.wons(3500000));
}

실제 프로젝트 적용 전략 💪

  1. 클래스 분리 기준
    • 변경의 이유가 다르다면 분리하기
    • 하나의 클래스가 여러 액터(사용자)를 위한 기능을 가진다면 분리하기
  2. 패키지 구조 예시
  3. com.company.employee ├── domain │ └── Employee.java # 직원 도메인 모델 ├── service │ ├── EmployeeService.java # 직원 서비스 │ └── PayCalculator.java # 급여 계산 ├── repository │ └── EmployeeRepository.java # 직원 저장소 └── report └── EmployeeReportGenerator.java # 리포트 생성
  4. 코드 리뷰 체크리스트
    • 클래스가 단일 책임을 가지는가?
    • 메서드가 한 가지 일만 하는가?
    • 변경이 발생할 때 한 클래스만 수정하면 되는가?

이해가 안 되는 부분이 있다면 댓글로 남겨주세요!
다음 시간에는 개방-폐쇄 원칙(OCP)에 대해 자세히 알아볼게요! 😊

728x90