본문 바로가기
프로그래밍/OOP

객체지향 프로그래밍의 설계원칙

by Daniel.kwak 2018. 10. 17.

목적

객체지향 프로그래밍의 5대원칙 'SOLID'를 이해한다.



*nextree의 객체지향에 관한 포스팅을 거의 필사하듯 따라서 쓰고 추가적인 기술적 설명을 덧붙였습니다. 

 http://www.nextree.co.kr/p6960/




1.Single Responsbility Principle, SRP (단일책임원칙)

클래스는 단 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어 있어야 한다. 어떤 변화에 의해 클래스를 변경해야 하는 이유는 단 하나여야 한다. SRP를 적용하여 리팩토링을 하면 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임의 변경으로 연쇄작용에서 자유로울 수 있다. 또한 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수 용이하나는 장점도 있다.




SRP를 적용하기 전이다. Serial Number는 변화요소가 아닌 고유 정보라 할 수 있다. 그러나 price,maker,type,model, 등 정보는 특성 정보군으로 변화가 발생할 수 있는 부분이다. A사에서 만든 기타1과 B사에서 만든 기타2가 다르듯이 변경이 발생할 수 있는 부분이다. 변경이 발생하면 기타 클래스를 항상 변경해야하는 부담이 발생한다. ->SRP의 적용 대상이다.



변화될 가능성이 있는 정보군들을 GuitarSpec으로 따로 관리를 하였다. 그리고 기타는 SerialNumber와 GuitarSpec object를 가지게 된다.

가독성이 좋아졌고 바뀔 수 있는 GuitarSpec만 따로 관리해주면 된다.

=>클래스 이름은 해당 클래스의 책임과 하나의 개념을 올바르게 나타내야 한다.



2.Open Close Principal,OCP(개방폐쇄의 원칙)

정의

확장에는 열려있고, 변경에는 닫혀있어야 한다는 원리이다. 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 한다는 의미로, 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 귑게 확장해서 재사용성 할 수 있어야 한다는 의미를 가진다.OCP를 가능하게 하는 중요 메커니즘은 추상화와 다형성이라고 로버트 C.마틴은 설명하고 있다.


적용방법

1.변경(확장)될 것과 변하지 않을 것을 엄격히 구분한다.

2.이 두 모듈이 만나는 지점에 인터페이스를 정의한다.

3.구현에 의존하기 보다는 정의한 인터페이스에 의존하도록 코드를 작성한다.


적용사례

위에서 다루었던 기타에서, 만약 바이올린, 트럼펫,콘트라베이스,드럼(!) 을 추가해야하는 상황이 온다면 어떻게 해야 할까? 

이렇듯 늘 변화를 염두에 두고 설계를 해야한다.




그 때 마다 해당 악기 클래스와 스펙 클래스를 생성할 수는 없을 것이다. 여기서 '추상화'를 적용해보자. 



이렇게 새로운 악기가 추가되면서 변경이 발생하는 부분을 추상화 하여 분리하였다. 코드의 수정을 최소화하여, 결합도는 줄이고 응집도는 높힐 있었다.


적용이슈

1.확장되는것과 변경되지 않는 모듈을 분리하는 과정에서 크지조절에 실패하면 오히려 관계가 복잡해질 수 있다.

2.인터페이스는 가능한 변경되면 안된다. 단 과도한 예측은 불필요한 작업을 만들고 상당한 비용이 발생한다.

3.적당한 추상화 레벨이 필요하다. 그레디 부치에 의하면 '추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징'이라고 정의한다.



3.The Loskov Substitution Princicple, LSP (리스코브 치환의 원칙)

정의

서브타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브타입은 언제나 기반 타입과 호환될 수 있어야 한다. 달리 말하면 서브타입은 기반 타입이 약속한 규약(public 인터페이스, 물론 메소드가 던지는 예외까지 포함) 을 지켜야 한다. 상속은 궁극적으로 다형성을 통한 확장성 획득을 목표로 한다. LSP원리는 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미한다. 일반적으로 선언은 기반 클래스로 생성은 구체 클래스로 대입하는 방법을 사용한다. 상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 IS-A 관계가 있을 경우로만 제한되어야 한다. 그 외에는 합성을 이용한 재사용을 해야 한다. 상속은 다형성과 따로 생각할 수 없다. 마찬가지로 다형성으로 인한 확장 효과를 얻기 위해서는 서브 클래스가 기반 클래스와 인터페이스 규약을 어겨서도 안된다. 결국 이 구조는 다형성을 통한 확장의 원리인 OCP를 제공하기도 한다. 객체지향의 설계 원리는 이렇게 서로가 서로를 이용하기도, 포함하기도 하는 특징이 있다. LSP는 규약을 준수하는 상속구조를 제공한다. 


적용방법

1.만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.

2.공통된 연산이 없다면 완전 별개인 2개의 클래스를 만든다.



void f(){  
    LinkedList list = new LinkedList();
    // …
    modify(list);
}

void modify(LinkedList list){  
    list.add(…);
    doSomethingWith(list);
}

List만 사용할거라면 문제가 되지 않지만 성능 향사을 위해 HashSet을 쓰는 상화이 발생한다면 LinkedList를 또 HashSet으로 언제 바꾸겠는가?

그러나 둘의 공통점은 Collection인터페이스를 상속하고 있다는 점이다.


void f(){  
     Collection collection = new HashSet();
     //…
     modify(list);
}

Void modify(Collection collection){  
     collection.add(…);
     doSomethingWith(collection);
}


커렉션 생성부분만 고치면 마음대로 어떤 커레ㅐㄱ션 구현 크래스를 사용할 숭 ㅣㅆ다. 이 프로그램은 lLSP와 OCP모두를 찾아볼 수 있는데 Colection인터페이스를 법용적으로 사용할 수 있는 점, (modify메소드가 잘 동작한다) modify메소드가 변화에는 닫혀있지만 커렉션의 변화와 확자에는 여려있다는 점이다. 




4.Interface Segregation Principle , ISP(인터페이스 분리의 원칙

정의

한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리이다. 즐 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 한다. 혹은 '하나의 일반적인 인터페이스보다는 여러개의 구체적인 인터페이스가 낫다' 라고 정의할 수도 있다. SRP가 클래스의 단일책임을 강조한다면 ISP는 인터페이스의 단일 책임을 강조한다. 하지만 ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 것을 인정한다. 이러한 경우 ISP에서는 인터페이스 분리를 통해 목표에 도달한다.


적용방법

1.클래스 인터페이스를 통한 분리

클래스 상속으로 인터페이스를 나눌 수 있다.

이 구조는 클라이언트에게 영향을 미치지 않고 인터페이스를 분리하는 효과를 지닌다->(?)

그러나 이러한 상속은 설계 시점에서 이미 정해지는게 일반적이다.

2.객체 인터페이스를 통한 분리

위임을 이용하여 나눈다. 만약 다른 클래스의 기능을 사용해야하지만 그 기능은 변경하고 싶지 않다면 상속 대신 위임을 사용한다.


적용사례

Java의 Swing JTable

JTable 안에는 많은 메소드가 존재한다. 컬럼 추가, 셀 에디터 리스너, 등등.. JTable 입장에서는 모두 제공해야 한다. JTable은 ISP방식대로 인터페이스를 분리하여 특정 역활만 수행할 수 있도록 한다. 즉, Accessible, CellEditorListener , ListSelectionListener , Scrollable, TableColumnModuleListner등등 여러 인터페이스를 모두 제공한다. 

import javax.swing.event.*;  
import javax.swing.table.TableModel;

public class SimpleTableDemo ... implements TableModelListener {  
    ...
    public SimpleTableDemo() {
        ...
        table.getModel().addTableModelListener(this);
        ...
    }
    //인터페이스를 통해 노출할 기능을 구현합니다.
    public void tableChanged(TableModelEvent e) {
        int row = e.getFirstRow();
        int column = e.getColumn();
        TableModel model = (TableModel)e.getSource();
        String columnName = model.getColumnName(column);
        Object data = model.getValueAt(row, column);

        ...// Do something with the data...
    }
    ...
}



5.Dependency Inversion Principle , DIP (의존성역전의 원칙)

정의

의존관계의 역전이랑 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전이다. 실제 사용 관계는 바뀌지 않으며 추상을 매개로 메세지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙이다.DIP의 주요 키워드는 IOC , 훅 메소드(슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드 할 수 있도록 만들어준 메소드. 서브클래스에서는 추상 메소드를 구현하거나 훅 메소드를 오버라이드 하는 방법을 이용해 기능을 확장) , 확장성이다. 이 세가지 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트 간의 커뮤니케이션을 효율적이게 한다.


적용방법

Layering, 잘 구조화된 객체지향 아키텍쳐들은 각 레이어마다 잘 정의되고 통제되는 인터페이스를 통한 긴밀한 서비스들의 집합을 제공하는 레이어들로 구성되어있다. 이것은 단순히 레이어를 통한 구조화만을 뜻하는게 아니라, Transitive Dependency가 발생했을때 상위 레벨의 레이어가 하위 레벨의 레이러를 바로 의존하게 하는게 아니라, 이 둘 사이에 존재하는 추상레벨을 통해 의존해야 할 것을 말하고 있다. 이를 통해 상위 레벨의 모듈은 하위 레벨의 모듈로의 의존성에서 벗어나 그 자체로 재사용성 되고 확장성도 보장받게 된다.



적용사례



public class Kid { private Toy toy; public void setToy(Toy toy){ this.toy = toy; } public void play(){ System.out.println(toy.toString()); } } public class Robot extends Toy { public String toString() { return "Robot"; } } public class Lego extends Toy { public String toString() { return "Lego"; } } public class Main { public static void main(String[] args) { Kid k = Kid(); // 1. 아이가 로봇을 가지고 놀 때 Toy t = new Robot(); // 2. 아이가 레고를 가지고 놀 때 Toy t = new Lego(); k.setToy(t); k.play(); } }
즉, 아이가 마음이 바뀌어 다른 어떠한 장난감으로 바꾸어 놀더라도 기존의 코드에는 영향을 미치지 않고 새로운 장난감을 추가해주기만 하면 된다.


Reference

1.http://www.nextree.co.kr/p6960/

2.https://gmlwjd9405.github.io/2018/07/05/oop-solid.html