객체지향프로그래밍 이란 무엇인가 (OOP)?  포스트에서 객체지향프로그램의 정의를 알아 보았습니다.

객체지향 프로그래밍 이란 캡슐화, 다형성, 상속 을 이용하여 코드 재사용을 증가시키고, 유지보수를 감소시키는 장점을 얻기 위해서 객체들을 연결 시켜 프로그래밍 하는 것 입니다.


정의를 알고 있다 해도 실제 구현에 적용하려 할때 모호함은 여전히 남아 있습니다. 객체지향설계는 나쁜 냄새(bad smell)을 제거하고 5개의 큰 원칙을 (Single Responsiblility Principle, Open-Closed Principle, Liskov Substitution Principle, Dependency Inversion Principle, Interface Segregation Principle) 지키려고 노력 할때 비로서 그 모양을 갖어갑니다.  Robert C. Martin 이 소개한 5가지 거대 원칙은 객체지향 설계의 근간이 되며, 소프트웨어 일반 책임 적용 패턴, 원칙 General Responsibility Assignment Software Patterns, GRASP  이라고 부르는 원칙에서 9가지 항목으로 세분화 됩니다.   

이번 포스트에서는 소프트웨어를 실패로 만드는 설계의 나쁜 냄새가 무엇이며, 객체지향으로 구현하기 위한 설계(Design) 원칙이 무엇인지 얘기해 보겠습니다.

 

Introduction

 우리는 무엇을 발견하려고 UML 다이어그램을 읽을까? 좋은 다이어 그램인지 아닌지 평가하는 기준은 무엇일까?

설계의 품질 - Quality of Design

잘 설계한 시스템은 이해하기 쉽고, 바꾸기 쉽고, 재사용하기 쉽습니다. 개발하는데 특별히 어렵지도 않고, 단순하고 간결하며 경제적입니다.

 

나쁜 설계의 냄새 - Smell of Bad Design

  1. 경직성 - 무엇이든 하나를 바꿀 때마다 반드시 다른 것도 바꿔야 하며, 그러고 나면 또 다른 것도 바꿔야 하는 변화의 사슬이 끊이지 않기 대문에 시스템을 변경하기 어렵다.

  2. 부서지기 쉬움 - 시스템에서 한 부분을 변경하면 그것과 전혀 상관없는 다른 부분이 작동을 멈춘다.( 바보같은 말이지만, 상관 있는 부분은 당연히 변경해야 한다.)

  3. 부동성 - 시스템을 여러 컴포넌트로 분해해서 다른 시스템에 재사용하기 힘들다.

  4. 끈끈함 - 개발 환경이 배관용 테이프나 풀로 붙인 것처럼 꽉 달라붙은 상태다. 편집-컴파일-테스트 순환을 한 번 도는 시간이 엄청나게 길다.

  5. 쓸데 없는 복잡함(과도한 디자인) - 괜히 머리를 굴려서 짠 코드 구조가 굉장히 많다. 이것들은 대개 지금 당장 하나도 필요 없지만 언젠가는 굉장히 유용할지도 모른다고 기대하고 만들었다.

  6. 필요 없는 반복 - 코드를 작성한 프로그래머 이름이 마치 '복사'와 '붙여넣기'같다.

  7. 불투명함 - 코드를 만든 의도에 대한 설명을 볼 때 그 설명에 '표현이 꼬인다'. 라는 말이 잘 어울린다.

  

Agile Software Development - Principles, Patterns, and Practices , Robert C. Martin 

 

로버트 c 마틴의 5원칙을 SOLID 라고 한다. SOLID Motivational Posters, by Derick Bailey

 

The Single Responsibility Principle - SRP

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐 이어야 한다.

 어떤 객체가 너무 많은 다른 종류의 행동을 할 수 있다면, 그것은  다른 객체가 변경 될 때 함께 변경되어야 할 가능성이 많다는 것을 의미합니다. 

가능한 하나 또는 명확하게 관련이 있는 책임을 부여 함으로써 객체는 보다 명확해(Explicit) 지고, 표현가능해 지며(Expressive), 느슨한 커플링(Loose Coupling) 과 높은 응집력(High Cohesion) 을 얻게 됩니다.

 

 The Open-Closed Principle - OCP

 소프트웨어 엔터티(클래스, 모듈, 함수 등등)는 Interface에 대해서는 개방되어야 하지만, 변경에 대해서는 폐쇄되어야 한다.

 단위 테스트를 할 때는 종종 환경에 생기는 변화를 제어하고 싶은 경우가 발생합니다. 예를 들어 Emploee를 어떻게 테스트 할지 생각해 보시죠. Emploee객체는 데이터베이스를 변경합니다. 하지만 테스트 환경에서 실제 데이터베이스를 바꾸고 싶지 않습니다. 그렇다고 해서 단위 테스트를 하기 위해 테스트용 더미 데이터베이스를 만들고 싶지도 않습니다. 그렇다면, 테스트 환경으로 환경을 변경해서 테스트 할 때 Emploee가 데이터베이스에 하는 모든 호출을 가로챈 다음 이 호출들이 올바른지 검증하면 좋을 것입니다.

 엔터티가 외부로 부터 영향을 받지 않고, 내부의 필요에 의해서만 변경하게 하려면 보호막으로 감싸고 노출되지 않아야 합니다. 그러나 다른 객체와 상호 작용하기 위해서는 선택적인 노출이 필요 합니다. 다른 객체와 대화하기 위한 인터페이스와 확장을 위해서는 Open 하고, 이외의 경우에는 Closed 해야 정보가 감추어지고 (Information Hiding) , 객체들끼리 연결하게 되는 끈이 줄어듦으로써 커플링이 타이트 해질 가능성이 줄어 듭니다.

Liskov Substitution Principle - LSP

서브타입(Sub Type)은 언제나 자신의 기반 타입(Base Type)으로 교체할 수 있어야 한다.

LSP에 따르면, 기반 클래스의 사용자는 그 기반 클래스에서 유도된 클래스를 기반 클래스로써 사용할때, 특별한 것을 할 필요 없이 마치 원래 기반 클래스를 사용하는 양 그대로 사용할 수 있어야 한다. 더 자세히 말하자면, instanceof나 다운캐스트를 할 필요가 없어야 합니다. 강조하건데, 사용자는 파생클래스에 대해서 아무것도 알 필요가 없어야 합니다. 파생 클래스가 있다는 사실 조차도.

 LSP는 상속(Inheritance), 다형성(Polymorphism) 과 관련된 원칙이죠. 상속은 코드 재사용 이라는 이유로 과용 될수 있는 기능입니다. 과잉 사용된 상속은 복잡한 계층구조와 커플링을 타이트하게 함으로써 객체지향으로 얻기 위한 유지관리비용 감소에 악영향을 미치는 요소중 하나 입니다. LSP란 상속을 사용할때 지켜야 하는 원칙을 말하는데요. 상속은 코드 재사용을 위해서가 아니라 명확한 가족 관계가 있을때 사용해야 하며, 그 행위(Behavior) 와 연관이 있습니다. 부모와 파생된 객체들이 존재 할때 가능한 모두 동일한 메소드 이름과 갯수를 가지게 함으로써  언제든지 서브 타입이 기반타입으로 교체될 수 있어야 함을 말합니다. 

다시 말하면, 상속의 오용은
가족이 아닌 객체를 비슷한 일을 하는 것을 보고 코드 재사용 욕심에 가족 관계로 묶는 것.
다층 상속으로 계층 구조가 깊고 복잡하게 하는 것.
파생 타입에 부모, 형제들과는 전혀 동떨어진 능력을 부여 함으로써 돌연변이를 만들어 버리는 것.
을 의미하며 LSP는 돌연변이(부모, 형제들의 형질과 관련이 없는 능력을 가진 파생타입 이며 이렇게 될때는 이미 가족이 아님을 의미)가 발생하지 않도록 관리해서 상속 관계의 타입들이 유연하게 대체 될 수 있게 하는 원칙 입니다. 

LSP의 더욱 자세한 설명은 LSP in Depth  포스트를 참조

 

Dependency Inversion Principle - DIP

  1. 고차원의 모듈은 저차원의 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존한다.
  2. 추상화 된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다. 

자주 변경되는 구상 클래스(Concreate class)에 의존하지 마라. 만약 어떤 클래스에서 상속받아야 한다면, 기반 클래스를 추상 클래스로 만들어라. 만약 어떤 클래스의 참조(Reference)를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라. 만약 어떤 함수를 호출해야 한다면, 호출 되는 함수를 추상 함수로 만들어야 합니다.

DIP는 그 용어 자체의 어려움 때문에 이해하기가 쉽지 않지만 내용은 단순합니다. 추상화된 것 (Absctract Class, Interface) 을 사용해야 커플링을 느슨하게 만들 수 있다는 말 입니다. 예를 들어

abstract class Car {} 로 부터 상속 받은 class Truck : Car {}, class Bus : Car {} 가 존재 할때,

Truck porter = new Truck() 과 같이 사용하지 말고,

Car myCar = new Truck();  과 같이 추상 클래스를 기반으로 작업하거나.


    abstract class Car
    {
       public abstract void Run();
       public abstract void Stop();
    }

    class Truck : Car
    {


        public override void Run()
        {
            throw new NotImplementedException();
        }

        public override void Stop()
        {
            throw new NotImplementedException();
        }
    }

    class Bus : Car
    {

        public override void Run()
        {
            throw new NotImplementedException();
        }

        public override void Stop()
        {
            throw new NotImplementedException();
        }
    }


    static void main()
    {
        Truck myCar = new Truck();
        myCar.Run();

        Car myCar = new Truck();
        myCar.Run();

    }

 인터페이스를 사용하는 경우.

  abstract class Car
    {

    }

    interface IDrive
    {
        public void Run();
        public void Stop();

    }

    class Truck : Car, IDrive
    {



        #region IDrive 멤버

        public void Run()
        {
            throw new NotImplementedException();
        }

        public void Stop()
        {
            throw new NotImplementedException();
        }

        #endregion
    }

    class Bus : Car, IDrive
    {


        #region IDrive 멤버

        public void Run()
        {
            throw new NotImplementedException();
        }

        public void Stop()
        {
            throw new NotImplementedException();
        }

        #endregion
    }


    static void main()
    {
        Car myCar = new Truck();
        IDrive drive = (IDrive)myCar;

        drive.Run();
        drive.Stop();
    }

 

와 같이 추상적인 것에 구체적인 것을 대입해 사용함으로써 커플링을 느슨하도록 만드는 원칙 입니다.

DIP 에 대한 상세한 개념에 대해서는 '의존 관계 역전의 원칙 Dependency Inversion Principle' 포스트를 읽어 보세요.


Interface Segregation Principle - ISP

클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다. 

비대한 클래스(Fat class) 란 메소드를 몇십 몇백개 가지는 클래스를 가르키는 말이다. 대개 시스템 안에 이런 클래스를 두고 싶어 하지 않지만, 가끔 피 할 수 없는 경우도 있습니다.

비대한 클래스가 거대하고 보기 흉하다는 사실 말고도, 한 사용자가 이 비대한 클래스의 메소드를 다 사용하는 일이 매우 적다는 것도 문제입니다. 즉. 메소드를 몇십개 선언한 클래스에서 사용자는 단지 두세 개만 호출 할 지도 모르죠. 불행하게도 이 사용자들은 호출하지도 않는 메소드에 생긴 변화에도 영향을 받습니다.


ISP 인터페이스 분리의 원칙은 가족관계가 아닐때 같은 행동을 하는 객체들에게 인터페이스를 할당 하고 사용 함으로써 DIP 를 가능하도록 만들고 , 그 결과로 느슨한 커플링과 명시성과 표현성을 얻습니다.





더 읽을 꺼리 :
* 객체지향 5원칙을 예제까지 포함하여 설명한 사내양 님의 글 
* SOLID 설명 캐스트

신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트


티스토리 툴바