카테고리 없음

[자바/스프링 개발자를 위한 실용주의 프로그래밍][chapter04] : SOLID, 의존성, SOLID & 객체지향, 디자인 패턴

j4ko 2024. 9. 8. 21:13
728x90
반응형

목차

     

    개요 

    : 객체지향에서 좋은 설계와 아키텍처를 이야기하면 나오는 개념인 SOLID이다.

    • SOLID는 로버트 마틴이 2000년대 초반에 고안한 5가지 원칙을 지칭하는 말이다.
      • 단일 책임 원칙 (SRP: Single Responsibility Principle)
      • 개방 폐쇄 원칙 (OCP: Open-Closed Principle)
      • 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)
      • 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
      • 의존성 역전 원칙(DIP: Dependency Inversion Principle)
    • 각 원칙은 객체지향 언어에서 좋은 설계를 얻기 위해 개발자가 지켜야 할 규범과 같은 것을 이야기한다. 그리고 각 원칙의 목표는 소프트웨어의 유지보수성과 확장성을 높이는 것이다.

    : 소프트웨어의 유지보수성을 높인다는 것은 무슨 뜻일까?

    • 설계 관점에서 코드의 유지보수성을 판단할 떄 사용할 수 있는 조금 더 실무적인 세 가지 맥락이 있다.
      • 영향 범위: 코드 변경으로 인한 영향 범위가 어떻게 되는가 ?
      • 의존성: 소프트웨어의 의존성 관리가 제대로 이뤄지고 있는가 ?
      • 확장성: 쉽게 확장 가능한가 ?

     

    즉, SOLID는 이 질문의 답을 알려주는 원칙인 것이다. SOLID를 따르는 코드는 코드 변경으로 인한 영향 범위를 축소할 수 있고, 의존성을 제대로 관리하며, 기능 확장이 쉽다.

     

    4.1 SOLID 소개

    4.1.1 단일 책임 원칙

    클래스를 변경해야할 이유는 단 하나여야 합니다.
    - 로버트 C. 마틴

    : 단일 책임 원칙은 클래스에 너무 많은 책임이 할당돼서는 안되며, 단 하나의 책임만 있어야 한다고 말한다.

    • 클래스는 하나의 책임만 갖고 있을 때 변경이 쉬워진다.
    • 단일 책임 원칙을 따르라는 말은 클래스가 특정 역할을 달성하는데만 집중할 수 있게 하라는 의미이다.
      • 클래스에 할당된 책임이 하나이면 코드를 이해하는 것도 쉽고 수정도 쉽다.

    : 단일 책임 원칙은 결국 “변경”과 연결된다.

    • 변경으로 인한 영향 범위를 최소화 하는 것이 이 원칙의 목적이다.
    • 소프트웨어는 복잡계이므로 빈번하게 들어오는 요구사항 변경을 효율적으로 처리하는 것이 중요하다. 따라서 외부의 변경요청에도 소프트웨어의 항성을 유지하려는 것이 이 원칙을 가장 큰 목적이다.

    : “책임”이란 무엇인가 ?

    • “책임”이란 말은 지나치게 추상적이다. 단일 책임 원칙이 말하는 “책임”은 무엇이며 어떨 때 이 원칙에 위배되는 걸까?
    • 단일 책임 원칙은 굉장히 단순한 원칙처럼 보이지만 협업하는 실무 레벨에서 이 원칙을 적용하는 것은 꽤나 어렵다.
      • 왜냐하면 책임은 문맥을 포함하는 개념이기 때문이며, 그로 인해 “책임”이라는 개념은 그것을 바라보는 개인이나 상황마다 다르게 해석될 여지가 있기 때문이다.
    • 따라서 책임이란 무엇이고 이를 어떻게 나눌지 기준이 필요하다.

     

    하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
    - 로버트 C 마틴

     

    : 로버트. C. 마틴은 단일 책임 원칙에 대해 위와 같이 말한다.

    • 액터는 메시지를 전달하는 주체이다. 그리고 단일 책임 원칙에서 말하는 책임은 액터에 대한 책임이다. 메시지를 요청하는 주체가 누구냐에 따라 책임이 달라질 수 있다.
      • 즉, 단일 책임을 이해하려면 “책임” 보다는 오히려 “액터에” 집중해야한다.
    • 시스템에서 어떤 모듈이나 클래스를 사용하게 될 액터가 몇명인지를 확인해야한다. 똑같은 코드일지라도 시스템에 따라 액터가 다를 수 있다.
      • 즉, 어떤 클래스를 사용하게 될 클래스를 사용하게 될 액터가 한 명이라면 단일 책임 원칙을 지키고 있는 것이고 여럿이라면 위반하고 있는 것이다.
      • 1. 어떤 모듈이나 클래스가 담당하는 액터가 혼자라면 단일 책임 원칙을 지키고 있는 것입니다. 2. 어떤 모듈이나 클래스가 담당하는 액터가 여럿이라면 단일 책임 원칙에 위배된다.
    • 따라서 단일 책임 원칙을 이해하려면 시스템에 존재하는 액터를 먼저 이해해야 한다. 그리고 그러기 위한 문맥과 상황이 필요하다.
      • 단일 책임 원칙에서 말하는 책임은 결국 액터에 대한 책임이기 때문이다.
      • 그리고 액터가 하나일 수 있다면 클래스를 변경할 이유도 하나로 고정된다. 바로 해당 액터의 요구사항이 변경될 때이다.
    • 버그나 시스템 성능 개선을 제외하고, 클래스를 변경할 이유는 유일한 액터의 요구사항이 변경될 때로 제한되어야 한다.
      • 액터가 여럿이라면 클래스를 변경해야 할 이유가 여럿일 것입니다. 여기저기 존재하는 여러 액터들의 요구사항이 변경될때마다 해당 클래스가 영향을 받기 때문이다.

    💡 단일 책임 원칙의 목표

    • 클래스가 변경됐을 때 영향을 받는 액터가 하나여야 한다.
    • 클래스가 변경할 이유는 유일한 액터의 요구사항이 변경될 때로 제한되어야 한다.

     

    4.1.2 개방 폐쇄 원칙

    : 개방 폐쇄 원칙은 주로 확장에 관한 이야기를 다룬다. 그래서 이 원칙은 확장에는 열려있고, 변경에는 닫혀있어야 한다 라는 말로 표현되기도 한다.

    클래스의 동작을 수정하지 않고 확장할 수 있어야 합니다.
    - 로버트 C. 마탄

    • 코드를 수정하지 않고 확장이 가능한 시스템을 만들어야하는 이유 ?
      • 시스템을 운영하면서 코드를 변경하는 것은 매우 위험하기 때문이다. 규모가 큰 시스템에서는 코드를 변경하는 것이 쉬운 일이 아니다.
      • 따라서 코드를 확장하고자 할 때 취할 수 있는 최고의 전략은 기존 코드를 아예 건드리지 않는 것이다.

     

    : OCP의 목표는 확장하기 쉬우면서도 변경으로 인한 영향 범위를 최소화하는 것이다.

    • 이 목표는 소프트웨어 설게에서 매우 중요한 가치로서, OCP 원칙은 “확장에는 열려있고 변경에는 닫혀 있다” 라는 말로 굉장히 간결하게 명확하게 표현하고 있다.
    • 또한 OCP 원칙은 코드를 추상화된 역할에 의존하게 만듦으로써 이를 달성할 수 있다.

     

    4.1.3 리스코프 치환 원칙

    : 리스코프 치환 원칙(LSP)은 “기본 클래스의 계약을 파생 클래스가 제대로 치환할 수 있는지 확인하라”는 원칙이다.

    파생 클래스는 기본 클래스를 대체할 수 있어야 한다.
    - 로버트 C. 마틴

    • 파생 클래스가 기본 클래스를 대체할 수 있는지 파악하기 위해 기본 클래스에 할당된 의도가 무엇인지 파악해야한다.
      • 파생 클래스는 기본 클래스에서 정의한 의도를 모두 지킬 수 있어야 한다.

     

    : 초기 코드 작성자의 의도를 파악하려면 어떻게 ?

    • 원시적으로는 코드 작성자에게 물어보는 방법이 있다.
      • 그러나 이는 불필요한 커뮤니케이션 비용을 발생시키며 초기 코드 작성자가 이미 퇴사한 상태일 수 있다.
        • 따라서 직접 물어봐서 문제를 해결하는 것은 그다지 좋은 방법이 아니다.

     

    : 코드 작성자의 의도를 드러낼 수 있는 조금 더 세련된 방법

    • 테스트 코드를 사용하는 것. 즉 초기 코드 작성자가 생각하는 모든 의도를 테스트 코드로 만들어 두는 것
      • 그렇게 할 수 있다면 파생 클래스를 작성하는 개발자는 테스트를 보고 초기 코드 작성자의 의도를 파악할 수 있을 것이고, 기본 클래스로 작성된 테스트에 파생 클래스를 추가해 테스트해 볼 수도 있을 것이다.
    • 인터페이스는 계약이며, 테스트는 계약 명세이다.

     

    4.1.4 인터페이스 분리 원칙

    : 인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다는 원칙이다.

    클라이언트별로 세분화된 인터페이스를 만드세요.
    - 로버트 C. 마틴

    • ISP는 어떤 클래스가 자신에게 필요하지 않은 인터페이스의 메서드를 구현하거나 의존하지 않아야 한다는 원칙이다.
      • 즉, 어떤 클래스가 자신에게 필요하지 않은 인터페이스의 메서드를 구현하거나 의존하지 않아야 한다는 말이다.
      • 이를 통해 인터페이스의 크기를 작게 유지하고, 클래스가 필요한 기능에만 집중할 수 있다.

     

    : 이 원칙은 개발자들이 하나의 인터페이스로 모든 것을 해결하려 할 때 위배된다.

    • 그러므로 이 원칙은 단일 책임 원칙과도 밀접한 관련이 있다.
    • 통합된 인터페이스는 구현체에 불필요한 구현을 강요할 수도 있다. 따라서 범용성을 갖춘 하나의 인터페이스를 만들기보다는 다수의 특화된 인터페이스를 만드는 편이 더 낫다.

     

    : 인터페이스를 통합하려는 시도는 응집도를 추구하는 행위일 수도 있다.

    • 하지만 그것이 곧 응집력이 높아지는 결과로 이어지는 것은 아니다. 왜냐하면 응집도라는 개념은 “유사한 코드를 한곳에 모은다”에서 끝나는 것이 아니기 때문이다.
      • 응집도의 종류는 다양하며 좀 더 세분화된 수준으로 다음과 같은 응집도가 있다.
        • 기능적 응집도, 순차적 응집도, 통신적 응집도, 절차적 응집도, 논리적 응집도
          • 응집도가 높은 순서
            • 기능 > 순차 > 통신 > 절차 > 논리
      • 모든 프로젝트에서 응집도가 높은 순서가 보장되는 것은 아니다. 프로젝트의 목적과 요구사항에 따라 이 순서는 달라질 수 있다. 따라서 순서를 외우고 각각을 구체적으로 완벽하게 이해할 필요는 없다.
      • “유사한 코드라서 한곳에 모은” 라는 접근은 “논리적 응집도”를 추구하는 방식이며 이는 다른 종류의 응집도보다 낮은 수준의 응집도를 추구하는 결과를 낳는다는 것
    • 인터페이스 분리하라는 말은 “기능적 응집도”를 추구하는 것이라 볼 수 있다.
      • 인터페이스는 곧 역할이라고 부를 수 있으며 ISP는 역할과 책임을 분리하고 역할을 세세하게 나누라는 의미다.

     

    : 원칙은 원칙이다.

    • 인터페이스를 분리했을 떄 얻을 수 있는 장점은 분명 많겠지만 원칙은 원칙일 뿐이다.
    • 설계는 줄다리기와 같아서 원칙과 효율성 사이에 잘 저울질 해야한다.

     

    4.1.4 의존성 역전 원칙

    : DIP는 고수준/저수준 모듈이 추상화에 의존해야 한다는 원칙이다.

    구체화가 아닌 추상화에 의존해야 한다.
    - 로버트 C. 마틴

    • 의존성 역전 원칙에 관핸 흔히 들을 수 있는 설명
      • 첫쨰. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야한다.
      • 둘째. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야한다.
    • 주요 포인트를 몇 가지로 다시 정리해보기
      • 고수준 모듈은 추상화에 의존해야한다.
      • 고수준 모듈이 저수준 모듈에 의존해서는 안된다.
      • 저수준 모듈은 추상화를 구현해야한다.

     

     

    4.2 의존성

    : 의존과 의존성이란 무엇인가?

    • 의존 : 다른 객체나 함수를 사용하는 상태
      • 다시 말해 어떤 객체가 다른 코드를 사용하고 있기만 해도 이를 가리켜 의존하고 있다고 볼 수 있다.
      • “의존”의 정의는 단순하고 명확하다. “사용하기만 해도 의존하는 것이다
        • 그렇기 떄문에 소프트웨어는 의존하는 객체들의 집합이라고 볼 수 있다.

     

    : 의존을 표현하는 또 다른 용어 “결합(coupling)”

    • 의존과 마찬가지로 어떤 객체나 코드를 사용하기만해도 결합이 생긴다고 할 수 있다.
      • 결합은 결합이 어떻게 되어 있느냐에 따라 강결합으로 평가되기도 하고 약결합으로 평가되기도 하다.
      • 일반적으로 소프트웨어에서는 결합도가 약하면 약할수록 좋다고 평가한다.

     

    : 약한 의존 상태로 만들고 유지하기는 어렵다

    • 우리는 어떻게하면 의존성을 약화시킬 수 있는지를 고민해야 한다.
    • 응집도와 마찬가지로 결합에도 다양한 종류가 있으며, 결합을 어떻게 하느냐에 따라 결합도를 다르게 평가받는다.
    • 소프트웨어 엔지니어는 시스템에 있는 결합도를 낮추고자 강한 의존 관계에서 약한 의존 관계를 새로 설계하기도 한다.

     

    : 의존성과 결합도를 낮추기 위한 몇 가지 기법

    • 의존성 주입 (dependency injection)

     

     

    4.2.1 의존성 주입

    : 의존성 주입이란 ?

    • 의존성 주입은 말 그대로 필요한 의존성을 외부에서 넣어주는(주입) 것을 의미한다.

     

    : 의존성 주입이 왜 의존성을 약화시키는 기법인가 ?

    • 의존성 주입은 의존성을 제거하지 않는다.
      • 의존성을 주입함으로써 필요한 객체의 수를 줄여나감으로 의존성을 약화시키는 기법이다.
    • 의존성 자체를 완전히 제거하기란 불가능하다.
      • 의존성 자체를 완전히 제거하려는 시도는 소프트웨어 개발에서는 협력 자체를 전면 부정하는 말이 된다.

     

    : 의존성 주입은 의존 개수를 줄이는 것 외에 불필요한 강한 의존이 생기지 않게 해준다.

    • “new 사용을 자제하라”
      • 자바를 다뤄본 사람이 한 번쯤 들어보는 격언
        • 코드를 작성하면서 new를 사용하는 것을 최대한 뒤로 미루고 자제하라는 것. 왜냐하면 new를 사용하는 것이 사실상 하드코딩이고 강한 의존성을 만드는 대표적인 사례라서 그렇다.
        • new”를 사용하는 순간 구현에 집중할 수 밖에 없다.
          • new를 사용하는 것은 여러 결합 유형 중 “Content Coupling”에 해당하는 결합이다., 정보 은닉이라는 설계 목적을 위반하는 사례이다.
          • 물론 개발하다 보면 결국 어딘가에는 new를 사용할 수 밖에 없지만 어딘가에서 객체를 인스턴스화하지 않는다면 시스템은 아무런 동작도 하지 않을 것이다.
      • 이 격언에서 하고 싶은 말은 상세한 구현 객체에 의존하는 것을 피하는 것
        • 구현 객체가 인스턴스화되는 시점을 최대한 뒤로 미루라는 것

     

    4.2.2 의존성 역전

    : 의존성 역전이란 무엇인가 ?

    • 추상화(interface)를 사용하고 추상을 사용하도록 코드를 변경하니, 원래는 의존을 당하던 객체가 의존을 하는 객체로 바뀌었다.
      • 이는 어마어마한 변화이며, “의존성 전이”라는 특징이다.
    • 의존성 역전 원칙
      • “세부 사항에 의존하지 않고 정책에 의존하도록 코드를 작성하라”라는 말로 바꿔 말할 수 있다.
    • “의존성 역전이 경계를 만든다”
      • 의존성 역전은 경계를 만드는 기법이며, 모듈의 범위를 정하고 상하 관계를 표현하는데 사용할 수 있는 수단이다.
        • 경계를 기준으로 모듈을 만들 수 있다.
    • 의존 방향을 고려해 의존성 역전을 적용하면 시스템을 플러그인처럼 어떤 모듈을 꽂느냐에 따라 다르게 동작할 수 있게 만들 수 있다.

     

    : 의존성 역전 원칙에 관한 로버트 C 마틴의 <클린 아키텍처>

    • “자바 같은 정적 타입 언어에서는 import 구문에 오직 인터페이스나 추상 클래스와 같은 추상 선언만 있어야 한다”라고 말했다.
    • 구현보다 추상에 의존하는 것이 좋다. 추상에 의존할 때 설계는 유연해지고, 변경에 자유로워질 수 있다.

     

    4.2.3 의존성 역전과 스프링

    : 의존성 역전은 원칙은 설계의 영역이다.

    • 따라서 의존성 역전 원칙을 지키고 싶다면 설계 부문에서 개발자들이 능동적으로 신경써야한다.

    : 스프링에서 사용하는 Controller → Service → Repository 순서

    • 해당 순서가 올바른 구조인지에 대해 생각해보고 의존성 역전을 도입해보자.

     

    4.2.4 의존성이 강조되는 이유

    : 유지보수성을 판단할 떄는 크게 세 가지 맥락

    • “영향 범위”에 문제가 있다면 응집도를 높이고 적절히 모듈화해서 단일 책임 원칙을 준수하는 코드를 만든다.
    • “의존성”에 문제가 있다면 의존성 주입과 의존성 역전 원칙 등을 적용해 약한 의존 관계를 만든다.
    • “확장성”에 문제가 있다면 의존성 역전 원칙을 이용해 개방 폐쇄 원칙을 준수하는 코드로 만든다.

     

    : 소프트웨어 설계를 잘하고 싶다면

    • 코드를 변경하거나 확장할 떄 영항받는 범위를 최소화 할 수 있어야 한다.
    • 코드의 영향 범위를 최소화하려면 의존성을 잘 다뤄야 한다.

     

    : 의존성을 잘 관리한다는 것은 ?

    • “불필요한 의존성을 줄인다”라는 목표를 포함하지만 의존성을 없애는 것이 목표가 아니다. 그보다는 “의존성을 끊는 것”이 목표다

     

    : “의존성을 끊는다는 것”은 무엇인가 ?

    • “의존성”은 컴포넌트 간의 상호작용을 표현하는 것
      • 한 컴포넌트가 변경되거나 영향을 받으면 관련된 다른 컴포넌트에도 영향이 갈 수 있다. 그런데 이렇게 영향을 받은 컴포넌트는 연쇄적으로 또 다른 관련 컴포넌트에 영향을 준다.
        • 이를 가리켜 “의존성이 전이된다”라고 표현한다.

     

    : 소프트웨어 설계는 복잡도와의 싸움이다.

    • 복잡한 의존성과 정리되지 않은 의존성은 스파게티 코드로 이어진다.
      • 스파게티 코드는 코드를 제대로 제멋대로 작성해서 발생하는 문제가 아니다. 의존 관계 관리가 제대로 안 되고 있으므로 발생하는 것이다.

     

    4.3 SOLID와 객체지향

    : SOLID 한 코드는 객체지향 코드다 ?

    • 엄격하게 말하자면 SOLID와 객체지향은 추구하는 바가 약간 다르다.
      • SOLID 원칙이 추구하는 것은 객체지향 설계다. 그래서 SOLID가 추구 방향과 객체지향이 추구하는 방향은 조금 다르다.
    • 객체지향의 핵심은 역할, 책임, 협력이다.
      • SOLID는 객체지향 방법론 중 하나로, 변경에 유연하고 확장할 수 있는 코드를 만드는데 초점을 둔다.
        • SOLID는 설계 원칙이고, 설계 원칙은 응집도를 높이고 의존성을 낮추는 방법에 집중한다.

     

    : 소프웨어 설계는 복잡계이다.

    • 요구사항과 100% 일치하는 해결책은 존재하지 않는다는 점을 항상 상기해야한다.
    • SOLID를 외우려 노력하기보다 SOLID가 해결하려 했던 문제와 추구했던 목표가 무엇이었는지 고심하는 편이 더 낫다.

     

     

    4.4 디자인 패턴

    : 디자인 패턴이란?

    • 소프트웨어 설계를 하면서 자주 만나게 되는 문제 상황을 정의하고, 이를 해결할 수 있는 모범 설계 사례를 모아 놓은 것이다.
      • 크게 생성, 구조, 행동 패턴으로 분류할 수 있다
        • 생성 패턴 : 객체 생성을 유연하고 효율적으로 처리하는 방법을 소개
        • 구조 패턴 : 객체를 조합해서 더 큰 구조를 형성하는 방법을 소개
        • 행동 패턴 : 객체 간의 행위와 역할을 조정하는 방법을 다룸
      • 이를 통해 객체 간의 상호작용을 개선하고 유연성을 높이는 것을 목표로 한다.

     

    : 디자인 패턴이 어떤 식으로 생겼고 예제를 외우려 하는 것은 좋은 공부 방법이 아니다.

    • 디자인 패턴을 학습하는 더 효율적인 방법은 디자인 패턴이 어떤 상황에서 어떤 문제를 해결하는지 이해하는 것이다.

     

    : 패턴이 무조건 최선도 아니다

    • 패턴을 사용하는 것이 문제의 규모에 비해 지나치게 과할 수도 있기 떄문이다. 패턴을 적용하고자 문제 상황을 억지로 변경할 이유도 없다.
    • 개발자의 업무는 문제를 해결하는 것이지 패턴을 적용하는 것이 아니다. 패턴은 도구일뿐이며 실제로 중요한것은 패턴에 담긴 “문제 인식”, “해결 과정”, “해결 방법”이다.
    728x90
    반응형