오늘은 SOLID 원칙을 하나씩 자세히 살펴보면서, 실제 코드로 어떻게 적용하는지 알아볼게요!
1. 단일 책임 원칙 (SRP) 이해하기 💡
핵심: "한 클래스는 단 하나의 변경 이유만 가져야 한다"
Bad Case: SRP 위반 사례
// ❌ 이렇게 하면 안돼요!
public class Employee {
public void calculatePay() { // 급여 계산
// 복잡한 급여 계산 로직
}
public void saveEmployee() { // DB 저장
// DB 저장 로직
}
public void generateReport() { // 리포트 생성
// 리포트 생성 로직
}
}
이게 왜 문제일까요? 🤔
- 급여 정책이 바뀌면 수정 필요
- DB가 변경되면 수정 필요
- 리포트 형식이 바뀌면 수정 필요
-> 세 가지 서로 다른 이유로 클래스가 변경될 수 있어요!
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);
}
}
장점이 뭘까요? 🌟
- 코드가 더 명확해져요
- 각 클래스가 하는 일이 분명함
- 클래스 이름만 봐도 역할을 알 수 있음
- 유지보수가 쉬워져요
- 급여 정책이 바뀌면 PayCalculator만 수정
- DB가 바뀌면 EmployeeRepository만 수정
- 리포트 형식이 바뀌면 EmployeeReportGenerator만 수정
- 테스트가 쉬워져요
@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);
}
}
단일 책임 원칙의 장점 정리 🎁
- 코드 가독성 향상
- 각 클래스의 책임이 명확해져서 코드를 이해하기 쉬워요
- 유지보수성 향상
- 변경이 필요할 때 관련 클래스만 수정하면 돼요
- 다른 부분에 영향을 주지 않아요
- 재사용성 향상
- 작은 단위로 분리되어 있어 필요한 기능만 가져다 쓸 수 있어요
- 테스트 용이성
- 각 책임별로 독립적인 테스트가 가능해요
// 테스트가 쉬워져요
@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));
}
실제 프로젝트 적용 전략 💪
- 클래스 분리 기준
- 변경의 이유가 다르다면 분리하기
- 하나의 클래스가 여러 액터(사용자)를 위한 기능을 가진다면 분리하기
- 패키지 구조 예시
com.company.employee ├── domain │ └── Employee.java # 직원 도메인 모델 ├── service │ ├── EmployeeService.java # 직원 서비스 │ └── PayCalculator.java # 급여 계산 ├── repository │ └── EmployeeRepository.java # 직원 저장소 └── report └── EmployeeReportGenerator.java # 리포트 생성
- 코드 리뷰 체크리스트
- 클래스가 단일 책임을 가지는가?
- 메서드가 한 가지 일만 하는가?
- 변경이 발생할 때 한 클래스만 수정하면 되는가?
이해가 안 되는 부분이 있다면 댓글로 남겨주세요!
다음 시간에는 개방-폐쇄 원칙(OCP)에 대해 자세히 알아볼게요! 😊
728x90
'800===Dev Docs and License > 이론 문서' 카테고리의 다른 글
객체지향 심화 학습 5편: 의존성 역전 원칙 (DIP) 완전정복 🎯 (0) | 2024.11.03 |
---|---|
객체지향 심화 학습 4편: 인터페이스 분리 원칙 (ISP) 완전정복 🎯 (1) | 2024.11.03 |
객체지향의 핵심 개념 정복하기 🎯 (0) | 2024.11.03 |
What is the Internet (0) | 2024.06.07 |
Coding Best Practice (0) | 2024.06.01 |