본문 바로가기

Programming/OOP

객체지향 원리 (headfirst OOA&D 에서 발췌)

매우 추상적인 얘기이며 어쩌면 당연하다고 생각했던 것들인데 실제 코딩에서는
무시하고 작성하는 버릇이 있는 듯 하여 기록해둡니다.
이 글을 보시는 분들중 다년간의 코딩의 경험이 있는 분이시면서도 이 개념에 대해서는 모르셨던 분이라면
'아하, 그것을 이렇게 표현하는구나' 라고 생각하실 수 있는 부분입니다.
저같은 초보는 이제 익숙해져야겠죠 ㅎㅎ;;

※ 밑의 나열된 원리들은 앞으로 코딩이나 디자인 패턴 학습을 통해서 예제나 경우로 채워나갈 것입니다.


1. 변하는 것을 캡슐화해주세요. 
   
   제가 이해하고 있는 위의 말은 행동에 의해 변화되는 데이터를 가지고 있는 객체에 대해서
   캡슐화를 하라는 말입니다. 저 위에 말했듯이 추상적인 얘기네요 ㅎㅎ
   제목에 적어놓은 서적에서 제공하는 예를 들어봅시다.

   darkship은 강아지가 원한다면 열리고 닫히는 강아지문을 현관문 하단에 설치하려고 합니다.
   이 강아지문은 강아지의 소리를 듣고 자동으로 열리고 시간의 흐름에 따라 자동으로 닫히거나 합니다.
   또한 darkship이 가지고 있는 리모콘을 이용하여 강아지문을 열고 닫을 수도 있습니다.

   위의 darkship 고객이 원하는 것을 찾아보면 리모콘에 따라서, 혹은 강아지의 소리를 판별하여
   문이 열리고 닫히는 강아지문을 원하네요. 
   그럼 여기서 캡슐화된 클래스로부터 생성되는 객체를 가져야 하는 것은 무엇일까요?
   답부터 말하자면 강아지문 입니다. 강아지문이라는 속성은 2가지의 상태를 가집니다.
   open, close 이죠. 따라서, 매우 필요한 data와 연산을 가지는 클래스로 구현을 해주어야합니다.
   딱, status, open(), close() 정도면 되겠군요.
   그러면 왜 나머지 클래스로 작성할 만한 요소들에 대해서는 캡슐화에 대해 민감하지 않아도 될까요?
   (위 문장의 말이 좀 이상하긴 하지만)
   변화하는 속성을 가지고 있지 않습니다.
   강아지는 아예 구현 자체가 필요없는 시스템 외부의 액터이며(darkship 도 마찬가지)
   리모콘은 강아지문의 상태변화만 알고 있으면 되는 것입니다.(1버튼 가정아래^^;;)
   그럼 실제로 필요한 data중 하나는 바로 강아지의 소리입니다.
   강아지의 소리를 알고 있어야 판별이 가능하니까요. 하지만 이 강아지의 소리는 변화하는 것이 아닙니다.
   강아지가 여러 소리를 낼 때마다 추가되어야하는 집합체(list)이죠.
   
   위에 작성하면서 리모콘을 잠시 얘기했는데 이에 대한 조심이 필요한 것을 얘기하기 위해 딴 얘길 하자면,
   객체지향 프로그래밍언어에서 늘 하는 말중 하나가 캡슐화라는 것이죠.
   headfirst OOA&D에서 얘기하는 것 중 하나가 변화하지 않는 것이 없다는 것 입니다.
   고객의 요구사항은 언제 어떻게 들어올지 모르며 우리가 작성한 소스코드는 언젠가 또 찾게 될지 모릅니다.
   리모콘이 원버튼이었으나 어느 고객은 리모콘의 디스플레이를 보고 강아지문을 다루고 싶다면
   이는 변화될 요지가 있는 부분입니다. (물론 그냥 디스플레이만 단순히 보여주면 상태를 알 필요가 없긴하겠지만요)
 

2. 구현에 의존하기보다는 인터페이스에 의존하도록 코딩하세요.

   이 말을 가능케 하기 위한 개념은 바로 다형성입니다. 제 블로그에 가장 많이 나오는 단어 중 하나가
    바로 다형성과 상속일 것입니다. 다형성이란 것은 부모 클래스를 상속하고 있는 자식 클래스가
    부모클래스처럼 행동이 가능하다는 것입니다.
    구현에 의존하지 말고 인터페이스에 의존하도록 하라는 것은
    계층구조가 하위 레벨인 객체를(상속단계에 있어서 낮은 단계)
    다른 외부 객체가 사용하는 것을 지양하고, 계층구조가 높은 레벨의 객체를 사용하라는 말입니다.
    밑에 여러 원리들에 묶이는 얘기이기도 한데요,
    상위 레벨은 가능한 변경요인에 대해 매우 둔감해야합니다. 즉, 추상화정도가 매우 높을수록
    특정 변경요인들에 대해 신경쓸 필요가 없도록 해야하는 것이란 말입니다.
    이러한 변경요인이 둔감한 녀석에 외부 객체가 의존하고 있다면
    외부 객체는 변경 요인에 의해서 잘못된 데이터를 다룰 일이 거의 없게 된다는 것이죠.
    예를 들어서, Animal 이라는 부모클래스를 의존하고 있는 외부 객체는
                      Dog라는 객체를 의존하는 것보다 월등히 변경요인에 둔감해집니다.
                      설계입장에서 잘 생각해야하는 것인데요, (예제를 위한 예이니 이상해도 참아주세요 ㅠㅠ)
                      Dog 객체에 의존한다면 Cat이라는 객체를 추가하거나 Mouse라는 객체를 추가하고자 할때
                      상당히 껄끄러운 작업이 예상될 수 밖에 없습니다.
                      하지만 Dog, Cat, Mouse라는 객체를 상속하고 있는 Animal이라는 객체에 의존한다면
                      하위 객체들의 추가나 변경에 있어서 신경쓸 필요가 없게되는 것이죠.
   첨언하자면, Animal이라는 객체에 의존하는 객체는 반드시 기능적인 면에 있어서
                    자신이 하려는 기능에만 집중해야합니다. 결합도를 줄이라는 말이죠...
                    정리한 제 자신도 어려운 말이네요 ㅎㅎ;;


3. 각 클래스는 변경 요인이 오직 하나이어야 합니다.
   
   클래스의 변경 이유가 하나 이상이라는 것은, 그 클래스가 너무 많은 일을 하려고 하는 것일 경우가 다수입니다.
   앞에서도 알려드렸듯이 클래스는 하나의 기능에 충실하여야 합니다.
   저도 많은 실수를 하고, 또 많은 객체지향 프로그래밍언어를 다루는 사람들이 하는 실수가
   하나의 클래스가 하고 있는 일을 편의에 따라서, 혹은 저처럼 작성중인 클래스에 대해 명확한 이해가 없어서..ㅠㅠ
   이 클래스가 해야할 일에 대해 마구잡이식으로 집어넣는 경우입니다.
   앞에서 한 얘기와 뒤에서 할 얘기들에서도 계속 얘기하는 것이지만,
   클래스가 뭘하는지에 대해 하나의 기능에 집중하도록 작성하세요.
   서적에서 제공한 예를 들어보겠습니다.
            Automobile 이라는 클래스에
                 start(), stop(), changeTires(Tire[]), drive(), wash(), checkOil(), getOil()
                 이라는 메소드가 정의되어 되어있다고 가정해봅니다.
                 출발하기, 멈추기, 타이어 바꾸기, 운전하기, 세차하기, 오일을 점검하기 등의 행동을 하는 것이
                 과연 자동차가 해야할까요?
            위의 클래스를 밑에 처럼 4개의 클래스로 나누면 어떨까요?
             Automobile : start(), stop(), getOil()
             CarWash : wash(Automobile)
             Driver : driver(Automobile)
             Mechanic : checkOil(Automobile), changeTires(Automobile, Tire[])
            위 4개의 클래스들은 각기 해야할 기능들이 명확합니다.
            Automobile 은 출발하고 정지하며, 필요에 의해서 기름을 채웁니다.
            CarWash 는 파라미터로 넘어오는 Automobile을 세차해줍니다.
            Dirver 는 파라미터로 넘어오는 Automobile을 운전합니다.
            Mechanic 은 기름을 체크하고 타이어를 교체합니다. // Mechanic은 경험으로 2개의 클래스로 나눌 수 있겠죠.
    첫번째 Automobile 클래스에서의 클래스 변경요인은
      1. 정비사가 오일 점검 방법을 바꾼다거나,
      2. 운전자가 다르게 운전하거나,
      3. 세차 방법이 개선되는 등입니다. 그때마다 Automobile 클래스를 변경해주어야합니다.
    두번째로 구분한 4개의 클래스들은 각 클래스들이 매우 응집도가 높아
    다른 클래스들을 변경하지 않으면서 자기 스스로는 변경하기 쉽게 되어있습니다.
    (물론 Mechanic 클래스는 1단계 더 나눌 수 있겠죠.)


4. 클래스는 행동과 기능에 관한 것입니다.
    
    약간 경험적인 부분이 아닌가 합니다.
    우리는 클래스를 정의할 때 이것이 객체라 생각합니다.
    그리고 여러 서적이나 학업에 있어서 객체는 실재하는 것이라고 배웠습니다.
    OOA&D 서적에서는 개념적으로 볼 수 있는 악기의 행동을 규정하기 위해
    추상클래스로 구현하였던 악기라는 클래스를 구상클래스로(일반 구현되는 클래스)
    설계 자체를 바꾸었습니다.
    서적에서 다룬 예를 들어서 작성하겠습니다.
    darkship은 기타같은 현악기를 판매하는 사람입니다.
    darkship은 기타 뿐만이 아니라 만돌린, 베이스, 반조와 같은 줄을 튕기는 현악기를 모두 판매하며
    개발팀에게 이러한 현악기를 추가하고, 찾아내는 프로그램을 의뢰하였습니다.
    만약에 '악기' 라는 추상클래스가 있고 이러한 클래스를 has a관계를 가지는 '창고'라는 클래스가 있다면
    모든 새로운 악기들이 추가될 때마다 악기 추상클래스를 상속하여 그 악기들을 클래스로 만들어주어야 합니다.
    하지만, 창고 클래스는 어떠한 클래스가 추가되던 추상클래스인 악기 클래스에 의존하기때문에
    추가적인 코딩이 필요하지 않습니다.
    이 말은, 어떠한 현악기가 추가가 되던 다른 클래스들은 의존하지 않기때문에 의미가 없다는 얘기죠.
    결국 프로그래머는 새로운 악기를 추가할 때마다 전혀 의미가 없는 클래스들을 추가해주어야 합니다.
    (추상 클래스는 객체로 생성되지 못하기 때문이죠.)
    이 말은 클래스가 무엇을 하느냐를 생각해 볼 필요가 있습니다.
    수많은 현악기 서브클래스가 하는 행동과 추상클래스인 악기 클래스가 하는 행동에 어떤 차이점이 있나요?
    물론 속성들은 다르겠지요, (이것은 새로운 클래스로 스펙클래스를 만들어서 빼주어 정의해주어야 맞겠죠)
    행동과 기능에 있어서 의미가 없기 때문에 악기라는 개념은
    추상클래스가 아닌 구상클래스로 작성해주어 불필요한 소스들을 제거해야하는 것입니다.
    이건 처음 설계때부터 많은 고민이 수반되어야할 것이고 경험이 바탕되어야하지 않을까 싶습니다.


5. 클래스는 확장에는 열려있고, 수정에는 닫혀 있어야 합니다. (Open-Closed Principle : OCP) 

   기존 코드를 변경하지 않으면서 코드의 수정을 허용한다는 원리입니다.
   잘 작성된 코드라면 변경할 필요 없이 추후에 확장만 할 필요가 있겠죠.
   역시 추상적인 표현인데요,
   Human 이라는 클래스가 name, gender, age, class 속성을 가지고 있고 이를 boolean matches(Human) 이라는
   메소드로 자신과 파라미터로 넘어온 객체를 비교한다고 가정합시다.
   만약 이 클래스가 잘 작성된 클래스라면 Student 라는 클래스가 이를 상속하고 같은 역할의 matches 라는 메소드를
   오버라이딩한다면 super.matches(..); 로 부모 메소드를 호출하고 Student 클래스의 나머지 속성들에 대해서
   선택적으로 data 비교를 하면 됩니다.
   하지만 잘못 작성된 클래스라면
   Human 클래스를 상속하고 있는 Employee 라는 클래스는 
   부모의 속성인 class 를 이용할 수 없고, 또한 부모의 메소드 matches 를 사용하지 말아야 합니다.
   결국 matches 메소드를 오버라이딩하되 부모의 메소드를 재사용하지 못하고 코드의 양이 길어지는 수고를 하게 됩니다.
   정리하자면, Student 라는 자식 클래스는 부모 클래스를 확장하였지만
                    Employee 라는 자식 클래스는 부모 클래스를 수정합니다.

6. 공통되는 부분을 추출하여 추상화하고 한 곳에 두어 중복 코드를 피해주세요.
   (Don't Repeat Yourself : DRY)


   이 원리는 코드 레벨뿐만이 아니라 요구사항 작성 레벨에서도 적용이 가능한 원리입니다.
   공통된 소스코드를 합치는 것이 전부가 아니라, 각 기능과 요구 사항을 한 번만 구현하려고 노력하는 것이
   DRY 원리의 핵심입니다.
   서적의 예를 든다면, 강아지의 문을 열었을 때 일정 시간이 지나면 자동으로 닫히는 기능에 대해서
                               강아지의 문을 열도록 지시하는 리모콘과 강아지 소리 인식기가
                               구현하고 있다면 같은 기능이 2번이나 구현이 된 것입니다.
                               타이머 시간을 체크하여 강아지의 문아 닫혀라 라고 해야하니까요.
                               이를 DRY의 원리를 이용한다면 한번의 코드작성으로 줄일 수 있습니다.
                               어차피 시간이 지나서 알아서 닫히길 바라는 것이라면
                               리모콘과 강아지 소리 인식기는 열려라라고 명령만 해주고
                               실제 타이머로 시간을 체크하여 문이 닫히는 기능은 강아지 문이 스스로 하게 해주면 됩니다.
                               강아지 문이 열렸을 때 시간체크하고 닫으면 되니까요.
                               이렇게 같은 기능이 2번 구현될 수 있는 것을 1번으로 줄였습니다.


7. 시스템의 모든 객체는 하나의 책임만을 가지며,
    객체가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어 있어야 한다.
    (단일 책임의 원리 / Single Responsibility Principle : SRP)


    이 원리는 응집도를 높이라는 요구로 바꿔말할 수 있습니다.
    DRY와 좀 헷갈리는 내용인데요, 제가 이해하고 있는 DRY와 SRP 원리는 이러합니다.
    DRY는 하나의 기능을 한 곳에 두자는 것이며, SRP는 클래스가 한 가지 일만 잘하게 하자는 것입니다.
    위 DRY에서 든 예제를 SRP 원리를 이용한다면 밑과 같이 해석할 수 있습니다.
               강아지문이라는 것이 하면 될 일인 타이머를 작동하여 일정 시간이 지난 후에
               강아지의 문을 닫는 기능을 강아지 문을 컨트롤 하는 리모콘과
               강아지의 문에 달려 있는 강아지 소리 인식기가 구현할 필요가 없는 것 입니다.
               각 클래스는 자신들의 할 기능에 대해서, 한 번만 구현되어야 하고
               이는 기능의 입장에서도 여러 클래스가 아닌 꼭 자신을 필요로 하는 클래스에 구현되어야합니다.

   
8. 서브 클래스들은 부모 클래스들이 사용되는 곳에 대체될 수 있어야 한다.
    (리스코프 치환 원리 / Liskov Substitution Principle : LSP)