ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체 지향 설계 원칙 SOILD, SRP와 OCP
    Java 2025. 1. 13. 09:02

     

    SOLID 5대 원칙에 대해 간단한 표로 정리하고, 그중 SRP와 OCP를 구체화하여 작성했습니다. 또한 SRP와 OCP를 위배한 코드와 부합한 코드 예시를 기록한 문서입니다.

     


    SOLID 5대 원칙

    SRP(Single Responsib
    ility Principle) 
    OCP(Open-Closed Principle ) LSP(Liskov Substitution Principle ) ISP(Interface Segregation Principle ) DIP(Dependency Inversion Principle )
    단일 책임 원칙 개방-폐쇄 원칙 리스코프 치환 원칙 인터페이스 분리 원칙 의존 역전 원칙
    클래스는 하나의 책임만 가져야 한다. or 클래스를 변경해야 하는 이유가 하나 뿐이어야 합니다. 클래스는 확장에는 열려있고 수정에는 닫혀있어야 한다. 부모 클래스 타입으로 자식 클래스 참조시, 프로그램 동작에 문제가 없어야 한다. 한 인터페이스에는 클라이언트가 실제로 필요로 하는 메서드만 있어야 한다. 고수준 모듈이 저수준 모듈에 의존하면 안 된다.

     


     

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

    클래스를 변경해야 하는 이유가 하나 뿐이어야 합니다.

     

    한 클래스에 여러 책임을 부여하면 예기치 않은 부작용이 발생할 수 있습니다.

    SRP는 이를 방지하고 프로그램의 안정성과 유지보수성을 높입니다.

    장점
    1. 유지보수의 용이성
    2. 변경의 이유가 명확함
    3. 의도하지 않은 부작용 방지
    4. 테스트 용이성

     

    "변경해야 하는 이유"란 무엇인가? : why보다는 who

    더보기
    더보기

    "변경해야 하는 이유"란 무엇인가?

     

    "한 클래스는 하나의 책임만 가져야 한다."로 외우곤 하는데, SRP 원칙을 제시한 Robert C. Martin은 실제로 다른 말을 했습니다. 단순히 하나의 책임을 가진다라고 이해하고 넘어간다고 큰일이 생기는 것도 아니라고 생각합니다.

    하지만 별개로, 이 원칙을 제시하게 된 Robert C. Martin이 블로그에 구체적으로 서술해놓은 글이 있기에 정리해둡니다.

    이 색으로 작성해둔 부분은 저 스스로에게 남겨두는 메모, 사담의 색이니 읽지 않고 넘어가셔도 됩니다.

     

    Robert C. Martin이 SRP를 제시하고 난 이후 "클래스를 변경해야하는 이유"라는 표현에 대해 질문이 많았다고 합니다. "버그 수정", " 리팩토링" 같은 것들이 변경의 이유가 될 수 있을까 하고요.

    Robert C. Martin은 버그 수정은 잘못된 구현을 고치는 것이지 코드의 책임 자체를 변경하는 것이 아니며, 리팩터링은 코드의 동작을 바꾸지 않고 내부 구조를 개선하는 작업으로, 역시 코드의 책임과는 무관하다고 말하며 "변경해야 하는 이유" 라는 것에 대해서 설명합니다.

     

    (생각 흐름 1) Robert C. Martin 이 말하는 "클래스의 변경"은, 단순히 코드 내용을 수정하는 행위 자체를 말하는 것이 아니라, 클래스가 담당하는 책임이 변경될 때 발생하는 수정을 의미하는 듯합니다.

    (생각 흐름 2)  또, 여기서 Robert C. Martin 이 말하는 "클래스의 책임"이란, 특정 비즈니스 요구사항을 충족하기 위해 클래스(주체)가 담당하는 역할을 의미합니다.

    (생각 흐름 3)  즉, 클래스의 비즈니스 요구사항 충족을 위해 만들어진 부분을 변경하게 되면 그것이  Robert C. Martin 가 말하는 "클래스 변경"이 되고, 이 변경의 이유가 하나여야 된다는 겁니다.

                            

    그럼 이제 본론으로 돌아가서 변경의 이유(변경해야하는 이유)는 무엇이 될까요? 아래는 SRP 원칙이 부합할 때와 위배할 때의 상황을 줄글로 적어놓은 것입니다.

     


     

     

    클래스에게 요구된 비즈니스 요구사항: 직원 관리

     

    ( SRP 원칙에 부합하는 클래스의 변경 예시 )

    상황 : 회사의 정책 변화에 따라 급여 계산 방식과 근무 시간 보고 방식이 변경됨.

    구체적인 변경 내용 :

                             시간제 급여 계산 -> 연봉제 급여 계산

                             근무 시간 보고 형태를 단순 Print로 보고 -> 근무 시간 보고 형태 JSON으로 보고

    변경 이유: 회사의 정책 변화라는 하나의 비즈니스 요구사항으로 인해 두 기능이 동시에 변경됨.

    두 변경 사항 모두 직원 관리라는 클래스의 책임 하에 있으며, 동일한 비즈니스 요구사항에 의해 변경되므로 SRP 원칙에 부합합니다.

     

    ( SRP 원칙을 위배하는 클래스의 변경 예시 )

    상황: 급여 계산 방식과 근무 시간 보고 방식이 서로 다른 이유로 변경됨.

    구체적인 변경 내용 :

                              시간제 급여 계산 -> 연봉제 급여 계산 (재무 부서 요청)

                              근무 시간 보고 형태를 단순 Print로 보고 -> 근무 시간 보고 형태 JSON으로 보고 (인사 부서 요청)

    변경 이유: 급여 계산은 재무 부서의 요청으로, 근무 시간 보고는 인사 부서의 요청으로 변경됨.

    두 변경 사항의 이유가 서로 다름에도 불구하고 이를 하나의 클래스에 두게 되면 SRP 원칙을 위배하게 됩니다.

     


     

     

    위 예시에서 이유의 공통점을 보면 "누군가가 바꾸라고 지시했다"입니다. 그 지시자가 공통일 때 SRP 원칙을 위배하지 않으며 지시자가 여럿으로 나눠질 땐 SRP 원칙을 위배하게 된다는 것입니다.

    누군가가 변경을 하라고 지시했고, 그 누군가는 "사람"입니다.

     

    However, as you think about this principle,

    remember that the reasons for change are people.

    It is people who request changes.

     

    (생각 흐름 4) 변경의 이유는 즉, 사람이란 것입니다. 변경의 이유를 이해할 때, why?(시간제보다 연봉제가 더 보기 쉬워) 보다는 who?(재무 부서가)를 중심으로 생각하면 이 글을 이해하기 편합니다.

     

    어떤 사람이 어떤 책임을 맡아 변경을 요청하는가, 그리고 그 사람은 단 하나여야 된다는 겁니다.

    그것이 "클래스를 변경해야 하는 이유가 하나 뿐이어야 한다"는 이렇게 이해될 수 있겠습니다.

     


     

    개인적으로 이 글을 정리하면서 생각한 건, SRP의 개념을 단순히 "하나의 클래스에 하나의 기능"으로 느낄 것이 아니라, 

    즉, 기능이라고 죄다 분리하는 것보단 같은 바운더리 안의 기능이라면 묶는 것도 충분히 SRP를 유지하는 좋은 방법이란 겁니다.

    Gather together the things that change for the same reasons.

    Separate those things that change for different reasons.

    같은 이유로 변화하는 것들을 모아보세요.

    다른 이유로 변화하는 것들을 분리하세요.

     

    Robert C. Martin의 블로그 글에서 SRP의 Another wording으로 제시하는 문장입니다. 무슨 말인지 느껴지시나요?!

     

    제가 읽고 생각한 걸 정리하고 있으나 틀린 부분이 있을 수도 있습니다. 혹시 틀린 이해나 제시할 점들이 있다면 댓글 편하게 작성해주십쇼!

     

    SRP를 위배한 코드와 부합한 코드 예시 : Employee 관리

    맨 위에 CEO가 있습니다. 그 CEO에게 보고하는 것은 CFO, COO, CTO 등 C-level 임원들입니다.

    CFO의 책임 : 회사 재정을 통제할 책임

    COO의 책임 : 회사 운영을 관리할 책임

    CTO 의 책임 : 회사 내의 기술 인프라, 개발 책임

    // SRP를 위반한 코드
    public class Employee {
      public Money calculatePay(); //CFO의 책임
      public void save(); //CTO의 책임
      public String reportHours(); //COO의 책임
    }

     

    Employee 클래스에 여러 개의 책임이 포함되어 있습니다.

     

    calculatePay() 메서드는 CFO의 책임인 급여 계산을 수행하고,

    save() 메서드는 CTO가 맡는 데이터 저장을 처리합니다.

    reportHours()는 COO의 책임인 근무 시간 보고를 담당합니다.

     

    각 책임이 독립적이지 않기 때문에, SRP를 위반하고 있습니다.

     

    // SRP 원칙에 부합한 코드
    
    public class PayCalculator { //CFO의 책임
        public Money calculatePay(Employee employee) {
            // 급여 계산 로직
        }
    }
    
    public class EmployeeSaver { //CTO의 책임
        public void save(Employee employee) {
            // 데이터 저장 로직
        }
    }
    
    public class HourReporter { //COO의 책임
        public String reportHours(Employee employee) {
            // 근무 시간 보고 로직
        }
    }

     

    각 기능을 독립적인 클래스로 분리하면 SRP에 부합하게 됩니다.

     


     

    OCP(Open-Closed Principle) : 개방-폐쇄 원칙

    클래스는 확장에는 열려있고 수정에는 닫혀있어야 한다.

     

    즉, 기능을 추가할 때 기존 코드를 변경하지 않고도 기능을 확장할 수 있도록 해야 한다는 의미입니다.

    시스템의 확장성을 높이고, 기존 시스템의 안정성을 유지하면서 새로운 기능을 추가할 수 있게 해줍니다.

     

    장점

    1. 확장 용이성
    2. 기존 코드의 안정성 보장
    3. 의도하지 않은 부작용 방지
    4. 테스트 용이성

     

    OCP를 위배한 코드와 부합한 코드 예시 : Payment

    // OCP 원칙을 위반한 코드
    class PaymentProcessor {
        public void processPayment(Payment payment) {
            if (payment.getType().equals("CreditCard")) {
                // 신용카드 결제 처리
            } else if (payment.getType().equals("Cash")) {
                // 현금 결제 처리
            }
            // 새로운 결제 방식이 추가될 때마다 여기 조건문을 수정해야 함
        }
    }

     

    결제 방식마다 조건을 추가해야 합니다.

    새로운 결제 방식이 추가되면, 매번 processPayment 메서드를 수정해야 하므로 OCP를 위반하고 있습니다.

     

    // OCP 원칙에 부합한 코드
    
    abstract class Payment {
        public abstract void processPayment();
    }
    
    class CreditCardPayment extends Payment {
        public void processPayment() {
            // 신용카드 결제 처리
        }
    }
    
    class CashPayment extends Payment {
        public void processPayment() {
            // 현금 결제 처리
        }
    }
    
    class PaymentProcessor {
        public void processPayment(Payment payment) {
            payment.processPayment(); // 각 결제 방식의 메서드를 호출
        }
    }

     

    Payment라는 추상 클래스를 만들고, CreditCardPayment와 CashPayment 클래스가 이를 상속받아 각각 processPayment 메서드를 구현합니다. 이제 새로운 결제 방식이 추가되면, PaymentProcessor 클래스를 수정할 필요 없이 새로운 클래스를 추가하고 processPayment만 구현하면 됩니다. 이렇게 하면 OCP에 부합하게 됩니다.

     

     


    참고

    The Single Responsibility Principle : https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

     

    Clean Coder Blog

    The Single Responsibility Principle 08 May 2014 In 1972 David L. Parnas published a classic paper entitled On the Criteria To Be Used in Decomposing Systems into Modules. It appeared in the December issue of the Communications of the ACM, Volume 15, Number

    blog.cleancoder.com

     

    SRP의 책임은 그 책임이 아니에요.https://blog.naver.com/wharup/223120145719

     

     

Designed by Tistory.