이끌든지 따르든지 비키든지

Software Development/Design Pattern

[Design Pattern] SRP(단일 책임 원칙)

SeongHo5 2024. 2. 17. 13:16

객체지향적인 프로그램 설계 시 가장 중요하게 여겨지며 반드시 지켜야 하는 5가지 원칙이 있습니다. 이들 원칙의 첫 글자를 따서 SOLID 원칙이라고 부릅니다. 오늘은 그중 첫 번째인 단일 책임 원칙(Single Responsibility Principle, SRP)에 대해 알아보겠습니다.


단일 책임 원칙(Single Responsibility Principle, SRP)

단일 책임 원칙은 객체가 오직 하나의 책임만을 가져야 한다는 원칙을 의미합니다. 즉, 객체는 오직 하나의 변경 이유만을 가져야 합니다. 이는 하나의 클래스가 하나의 책임(기능)만을 담당하여 그 책임을 수행해야 한다는 의미로 해석될 수 있습니다.

 

현실 세계에서 맥가이버칼과 같은 다기능 도구는 여러 작업을 한 번에 해결할 수 있는 편리함을 제공합니다. 

여러 도구를 하나씩 들고 다니는 수고를 줄여주는 유용한 도구지만, 소프트웨어 개발의 맥락에서 맥가이버칼을 하나의 객체로 보자면, 단일 책임 원칙을 위배하는 예로 볼 수 있습니다.

맥가이버칼

 

하나의 클래스(맥가이버칼)가 여러 기능(톱질, 칼, 가위, 드라이버···)을 수행하고 있기 때문입니다.

 

소프트웨어 개발에서는 '만능 도구'보다는 각각의 목적과 기능에 맞춰 특화된 개별 도구들을 사용하는 것이 유리합니다.

 


왜? 한 클래스는 하나의 책임만을 가져야 할까?

 

 

가장 큰 이유는 유지보수성에 있습니다.

 

한 클래스가 여러 기능을 담당하게 되, 클래스 내의 코드들이 서로 다른 역할을 수행하면서 강한 결합도를 가지게 될 가능성이 높아집니다.

 

결합도가 높아지면 시스템 전체의 복잡성이 증가하고, 특정 기능의 변경이 다른 부분에서 예기치 않은 오류를 발생시킬 위험이 커집니다.

 

이는 결국 다른 기능의 수정을 불가피하게 만들어 시스템의 안정성을 저해하고 유지보수를 어렵게 만듭니다.

 

💡결합도 & 응집도란?

결합도는 클래스나 모듈 간의 의존성 정도를 나타내며, 낮을수록 서로 독립적이고 유지보수가 쉬워집니다. 

응집도는 모듈 내부의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타내며, 높을수록 모듈이 한 가지 기능에 집중하고 있음을 의미합니다. 

이상적인 소프트웨어 설계는 낮은 결합도와 높은 응집도를 지향해야 합니다.

 

 

단일 책임 원칙을 준수하는 코드는 각 클래스가 명확하게 정의된 하나의 책임을 가지므로, 코드의 가독성이 향상되고, 각 기능을 이해하고 수정하기가 더 쉬워집니다. 

 

또한, 특정 기능에 대한 변경이 다른 기능에 영향을 미치지 않으므로, 시스템의 안정성이 유지되며, 기능의 추가나 수정이 간편해집니다. 

이러한 코드는 재사용이 용이하고, 단위 테스트가 간단해져 테스트 주도 개발(TDD)과 같은 개발 방법론을 적용하기에도 적합합니다. 

 


코드로 알아보기

 

단일 책임 원칙을 위배한 코드 예시와 이를 수정한 예시를 통해 알아보겠습니다.

 

public class OrderService {

    public void processOrder() {
        // 주문 처리
    }
    
    public void cancelOrder() {
        // 주문 취소
    }

    private void sendEmail() {
        // 이메일 전송
    }
}

 

OrderService 클래스는 현재 두 가지 책임(주문 처리 및 이메일 전송)을 가지고 있습니다. 

 

 


 

 

리팩토링을 통해, 하나는 주문(Order)을 담당하고, 다른 하나는 이메일 전송(EmailService)을 담당하도록, 두 개의 클래스로 분리합니다.

 

public class OrderService {
    public void processOrder() {
        // 주문 처리
    }
    
    public void cancelOrder() {
        // 주문 취소
    }
}


public class EmailService {
    public void sendEmail() {
        // 이메일 전송
    }
}

 

이 리팩토링을 통해, 이를 통해 각 클래스는 자신의 책임에만 집중할 수 있으며, 이는 유지보수성, 확장성, 재사용성을 향상시킬 수 있습니다.

 

 

주의할 점

  • 적절한 수준의 추상화 유지: 각 클래스와 메서드는 적절한 수준의 추상화를 유지해야 합니다. 너무 많이 분리된 로직은 파편화를 초래하고, 코드를 이해하는 데 더 많은 시간을 소비되어 오히려 유지보수성이 떨어질 수 있습니다.
  • 코드 재사용성과 유지보수성의 균형: 클래스를 분리할 때는 재사용성과 유지보수성 사이의 균형을 고려해야 합니다. 너무 세분화된 클래스는 재사용성을 높일 수 있지만, 동시에 유지보수를 복잡하게 만들 수 있습니다.