[CS] 면접 대비 디자인패턴

[Design Pattern] 개요

  • 일종의 설계 기법이며, 설계 방법이다.
목적
SW 재사용성, 호환성, 유지 보수성을 보장.
특징
디자인 패턴은 아이디어임, 특정한 구현이 아님.

프로젝트에 항상 적용해야 하는 것은 아니지만, 추후 재사용, 호환, 유지 보수시 발생하는 문제 해결을 예방하기 위해 패턴을 만들어 둔 것임.

원칙 - SOLID (객체지향 설계 원칙)

(간략한 설명)

1. Single Responsibility Principle

하나의 클래스는 하나의 역할만 해야 함.

2. Open - Close Principle

확장 (상속)에는 열려있고, 수정에는 닫혀 있어야 함.

3. Liskov Substitution Principle

자식이 부모의 자리에 항상 교체될 수 있어야 함.

4. Interface Segregation Principle

인터페이스가 잘 분리되어서, 클래스가 꼭 필요한 인터페이스만 구현하도록 해야함.

5. Dependency Inversion Property

상위 모듈이 하위 모듈에 의존하면 안됨.

둘 다 추상화에 의존하며, 추상화는 세부 사항에 의존하면 안됨.

분류 (중요)
  • 3가지 패턴의 목적을 이해하기!
1. 생성 패턴 (Creational) : 객체의 생성 방식 결정

Class-creational patterns, Object-creational patterns.

  • 예) DBConnection을 관리하는 Instance를 하나만 만들 수 있도록 제한하여, 불필요한 연결을 막음.
2. 구조 패턴 (Structural) : 객체간의 관계를 조직
  • 예) 2개의 인터페이스가 서로 호환이 되지 않을 때, 둘을 연결해주기 위해서 새로운 클래스를 만들어서 연결시킬 수 있도록 함.
3. 행위 패턴 (Behavioral): 객체의 행위를 조직, 관리, 연합
  • 예) 하위 클래스에서 구현해야 하는 함수 및 알고리즘들을 미리 선언하여, 상속시 이를 필수로 구현하도록 함.

어댑터 패턴(Adapter Pattern)

  • 클래스를 바로 사용할 수 없는 경우가 있음 (다른 곳에서 개발했다거나, 수정할 수 없을 때)

  • 중간에서 변환 역할을 해주는 클래스가 필요 → 어댑터 패턴

사용 방법 : 상속
호환되지 않은 인터페이스를 사용하는 클라이언트 그대로 활용 가능
향후 인터페이스가 바뀌더라도, 변경 내역은 어댑터에 캡슐화 되므로 클라이언트 바뀔 필요X
[클래스 다이어그램]

20211120_174252

아이폰의 이어폰을 생각해보자

가장 흔한 이어폰 잭을 아이폰에 사용하려면, 잭 자체가 맞지 않는다.

따라서 우리는 어댑터를 따로 구매해서 연결해야 이런 이어폰들을 사용할 수 있다

이처럼 어댑터는 필요로 하는 인터페이스로 바꿔주는 역할을 한다

20211120_174818

이처럼 업체에서 제공한 클래스가 기존 시스템에 맞지 않으면?

  • 기존 시스템을 수정할 것이 아니라, 어댑터를 활용해 유연하게 해결하자
코드로 어댑터 패턴 이해하기
오리와 칠면조 인터페이스 생성

만약 오리 객체가 부족해서 칠면조 객체를 대신 사용해야 한다면?

두 객체는 인터페이스가 다르므로, 바로 칠면조 객체를 사용하는 것은 불가능함

따라서 칠면조 어댑터를 생성해서 활용해야 한다
  • Duck.java
package AdapterPattern;

public interface Duck {
	public void quack();
	public void fly();
}
  • Turkey.java
package AdapterPattern;

public interface Turkey {
	public void gobble();
	public void fly();
}
  • WildTurkey.java
package AdapterPattern;

public class WildTurkey implements Turkey {

	@Override
	public void gobble() {
		System.out.println("Gobble gobble");
	}

	@Override
	public void fly() {
		System.out.println("I'm flying a short distance");
	}
}
  • TurkeyAdapter.java
package AdapterPattern;

public class TurkeyAdapter implements Duck {

	Turkey turkey;

	public TurkeyAdapter(Turkey turkey) {
		this.turkey = turkey;
	}

	@Override
	public void quack() {
		turkey.gobble();
	}

	@Override
	public void fly() {
		turkey.fly();
	}

}
  • DuckTest.java ``` package AdapterPattern;

public class DuckTest {

public static void main(String[] args) {

	MallardDuck duck = new MallardDuck();
	WildTurkey turkey = new WildTurkey();
	Duck turkeyAdapter = new TurkeyAdapter(turkey);

	System.out.println("The turkey says...");
	turkey.gobble();
	turkey.fly();

	System.out.println("The Duck says...");
	testDuck(duck);

	System.out.println("The TurkeyAdapter says...");
	testDuck(turkeyAdapter);

}

public static void testDuck(Duck duck) {

	duck.quack();
	duck.fly();

} } ``` 아까 확인한 클래스 다이어그램에서 Target은 오리에 해당하며, Adapter는 칠면조라고 생각하면 된다.

싱글톤 패턴(Singleton pattern)

  • 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴
즉, 싱글톤 패턴은 ‘하나’의 인스턴스만 생성하여 사용하는 디자인 패턴이다.
  • 인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것!

생성자가 여러번 호출되도, 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것이다

(java에서는 생성자를 private으로 선언해 다른 곳에서 생성하지 못하도록 만들고, getInstance() 메소드를 통해 받아서 사용하도록 구현한다)

왜 쓰나요? 먼저, 객체를 생성할 때마다 메모리 영역을 할당받아야 한다. 하지만 한번의 new를 통해 객체를 생성한다면 메모리 낭비를 방지할 수 있다.

또한 싱글톤으로 구현한 인스턴스는 ‘전역’이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능한 장점이 있다.

많이 사용하는 경우가 언제인가요? 주로 공통된 객체를 여러개 생성해서 사용해야하는 상황

  • 데이터베이스에서 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등

안드로이드 앱 : 각 액티비티 들이나, 클래스마다 주요 클래스들을 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계

또한 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용함

단점도 있나요? 객체 지향 설계 원칙 중에 개방-폐쇄 원칙이란 것이 존재한다.

만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이때 개방-폐쇄 원칙이 위배된다.

결합도가 높아지게 되면, 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.

또한, 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제도 발생할 수 있다.

따라서, 반드시 싱글톤이 필요한 상황이 아니면 지양하는 것이 좋다고 한다. (설계 자체에서 싱글톤 활용을 원활하게 할 자신이 있으면 괜찮음)

멀티스레드 환경에서 안전한 싱글톤 만드는 법
1. Lazy Initialization (게으른 초기화)
public class ThreadSafe_Lazy_Initialization{

    private static ThreadSafe_Lazy_Initialization instance;

    private ThreadSafe_Lazy_Initialization(){}

    public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
        if(instance == null){
            instance = new ThreadSafe_Lazy_Initialization();
        }
        return instance;
    }

}

private static으로 인스턴스 변수 만듬

private으로 생성자를 만들어 외부에서의 생성을 막음

synchronized 동기화를 활용해 스레드를 안전하게 만듬

  • 하지만, synchronized는 큰 성능저하를 발생시키므로 권장하지 않는 방법
2. Lazy Initialization + Double-checked Locking
  • 1번의 성능저하를 완화시키는 방법
public class ThreadSafe_Lazy_Initialization{
    private volatile static ThreadSafe_Lazy_Initialization instance;

    private ThreadSafe_Lazy_Initialization(){}

    public static ThreadSafe_Lazy_Initialization getInstance(){
    	if(instance == null) {
        	synchronized (ThreadSafe_Lazy_Initialization.class){
                if(instance == null){
                    instance = new ThreadSafe_Lazy_Initialization();
                }
            }
        }
        return instance;
    }
}

1번과는 달리, 먼저 조건문으로 인스턴스의 존재 여부를 확인한 다음 두번째 조건문에서 synchronized를 통해 동기화를 시켜 인스턴스를 생성하는 방법

스레드를 안전하게 만들면서, 처음 생성 이후에는 synchronized를 실행하지 않기 때문에 성능저하 완화가 가능함

  • 하지만 완전히 완벽한 방법은 아님
3. Initialization on demand holder idiom (holder에 의한 초기화)

클래스 안에 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법

public class Something {
    private Something() {
    }

    private static class LazyHolder {
        public static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

2번처럼 동기화를 사용하지 않는 방법을 안하는 이유는, 개발자가 직접 동기화 문제에 대한 코드를 작성하면서 회피하려고 하면 프로그램 구조가 그만큼 복잡해지고 비용 문제가 발생할 수 있음. 또한 코드 자체가 정확하지 못할 때도 많음

이 때문에, 3번과 같은 방식으로 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용해 싱글톤의 초기화 문제에 대한 책임을 JVM에게 떠넘기는 걸 활용함

클래스 안에 선언한 클래스인 holder에서 선언된 인스턴스는 static이기 때문에 클래스 로딩시점에서 한번만 호출된다. 또한 final을 사용해서 다시 값이 할당되지 않도록 만드는 방식을 사용한 것

  • 실제로 가장 많이 사용되는 일반적인 싱글톤 클래스 사용 방법이 3번이다.

템플릿 메소드 패턴(Template Method Pattern)

  • 로직을 단계 별로 나눠야 하는 상황에서 적용한다.

  • 단계별로 나눈 로직들이 앞으로 수정될 가능성이 있을 경우 더 효율적이다.

조건
  • 클래스는 추상(abstract)로 만든다.

  • 단계를 진행하는 메소드는 수정이 불가능하도록 final 키워드를 추가한다.

  • 각 단계들은 외부는 막고, 자식들만 활용할 수 있도록 protected로 선언한다.

예를 들어보자. 피자를 만들 때는 크게 반죽 → 토핑 → 굽기 로 3단계로 이루어져있다.

이 단계는 항상 유지되며, 순서가 바뀔 일은 없다. 물론 실제로는 도우에 따라 반죽이 달라질 수 있겠지만, 일단 모든 피자의 반죽과 굽기는 동일하다고 가정하자. 그러면 피자 종류에 따라 토핑만 바꾸면 된다.

abstract class Pizza {

    protected void 반죽() { System.out.println("반죽!"); }
    abstract void 토핑() {}
    protected void 굽기() { System.out.println("굽기!"); }

    final void makePizza() { // 상속 받은 클래스에서 수정 불가
        this.반죽();
        this.토핑();
        this.굽기();
    }

}
class PotatoPizza extends Pizza {

    @Override
    void 토핑() {
        System.out.println("고구마 넣기!");
    }

}

class TomatoPizza extends Pizza {

    @Override
    void 토핑() {
        System.out.println("토마토 넣기!");
    }

}

abstract 키워드를 통해 자식 클래스에서는 선택적으로 메소드를 오버라이드 할 수 있게 된다.

abstract 키워드를 통해 자식 클래스에서는 선택적으로 메소드를 오버라이드 할 수 있게 된다.

abstract와 Interface의 차이는?
1. abstract : 부모의 기능을 자식에서 확장시켜나가고 싶을 때
2. interface : 해당 클래스가 가진 함수의 기능을 활용하고 싶을 때
3. abstract는 다중 상속이 안된다. 상황에 맞게 활용하자!

팩토리 메소드 패턴(Factory Method Pattern)

-객체를 만드는 부분을 Sub class에 맡기는 패턴

Robot (추상 클래스)
- SuperRobot
- PowerRobot

RobotFactory (추상 클래스)
- SuperRobotFactory
- ModifiedSuperRobotFactory

즉 Robot이라는 클래스를 RobotFactory에서 생성함.

  • RobotFactory 클래스 생성
    public abstract class RobotFactory {
      abstract Robot createRobot(String name);
    }
    
  • SuperRobotFactory 클래스 생성
    public class SuperRobotFactory extends RobotFactory {
      @Override
      Robot createRobot(String name) {
          switch(name) {
          case "super" :
              return new SuperRobot();
          case "power" :
              return new PowerRobot();
          }
          return null;
      }
    }
    

생성하는 클래스를 따로 만듬

그 클래스는 factory 클래스를 상속하고 있기 때문에, 반드시 createRobot을 선언해야 함.

name으로 건너오는 값에 따라서, 생성되는 Robot이 다르게 설계됨.

정리하면, 생성하는 객체를 별도로 둔다. 그리고, 그 객체에 넘어오는 값에 따라서, 다른 로봇 (피자)를 만들어 낸다.

옵저버 패턴(Observer pattern)

  • 상태를 가지고 있는 주체 객체 & 상태의 변경을 알아야 하는 관찰 객체

(1 대 1 or 1 대 N 관계)

서로의 정보를 주고받는 과정에서 정보의 단위가 클수록, 객체들의 규모가 클수록 복잡성이 증가하게 된다. 이때 가이드라인을 제시해줄 수 있는 것이 ‘옵저버 패턴’

주체 객체와 관찰 객체의 예는?
  • 잡지사 : 구독자
  • 우유배달업체 : 고객

구독자, 고객들은 정보를 얻거나 받아야 하는 주체와 관계를 형성하게 된다. 관계가 지속되다가 정보를 원하지 않으면 해제할 수도 있다. (잡지 구독을 취소하거나 우유 배달을 중지하는 것처럼)

  • 이때, 객체와의 관계를 맺고 끊는 상태 변경 정보를 Observer에 알려줘서 관리하는 것을 말한다.

20211120_183026

  • Publisher 인터페이스

Observer들을 관리하는 메소드를 가지고 있음 옵저버 등록(add), 제외(delete), 옵저버들에게 정보를 알려줌(notifyObserver)

public interface Publisher {
    public void add(Observer observer);
    public void delete(Observer observer);
    public void notifyObserver();
}
  • Observer 인터페이스
    정보를 업데이트(update)
public interface Observer {
    public void update(String title, String news); }
  • NewsMachine 클래스 :Publisher를 구현한 클래스로, 정보를 제공해주는 퍼블리셔가 됨
public class NewsMachine implements Publisher {
    private ArrayList<Observer> observers;
    private String title;
    private String news;

    public NewsMachine() {
        observers = new ArrayList<>();
    }

    @Override public void add(Observer observer) {
        observers.add(observer);
    }

    @Override public void delete(Observer observer) 	{
        int index = observers.indexOf(observer);
        observers.remove(index);
    }

    @Override public void notifyObserver() {
        for(Observer observer : observers) {
           observer.update(title, news);
        }
    }

    public void setNewsInfo(String title, String news) {
        this.title = title;
        this.news = news;
        notifyObserver();
    }

    public String getTitle() { return title; } 		public String getNews() { return news; }
}
  • AnnualSubscriber, EventSubscriber 클래스
    Observer를 구현한 클래스들로, notifyObserver()를 호출하면서 알려줄 때마다 Update가 호출됨
public class EventSubscriber implements Observer {

    private String newsString;
    private Publisher publisher;

    public EventSubscriber(Publisher publisher) {
        this.publisher = publisher;
        publisher.add(this);
    }

    @Override
    public void update(String title, String news) {
        newsString = title + " " + news;
        display();
    }

    public void withdraw() {
        publisher.delete(this);
    }

    public void display() {
        System.out.println("이벤트 유저");
        System.out.println(newsString);
    }

}

Java에는 옵저버 패턴을 적용한 것들을 기본적으로 제공해줌

  • Observer 인터페이스, Observable 클래스

하지만 Observable은 클래스로 구현되어 있기 때문에 사용하려면 상속을 해야 함. 따라서 다른 상속을 함께 이용할 수 없는 단점 존재

정리

옵저버 패턴은, 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이 가고, 자동으로 정보가 갱신되는 1:N 관계(혹은 1대1)를 정의한다.

인터페이스를 통해 연결하여 느슨한 결합성을 유지하며, Publisher와 Observer 인터페이스를 적용한다.

안드로이드 개발시, OnClickListener와 같은 것들이 옵저버 패턴이 적용된 것 (버튼(Publisher)을 클릭했을 때 상태 변화를 옵저버인 OnClickListener로 알려주로독 함)


스트레티지 패턴(Strategy Pattern)

  • 어떤 동작을 하는 로직을 정의하고, 이것들을 하나로 묶어(캡슐화) 관리하는 패턴

새로운 로직을 추가하거나 변경할 때, 한번에 효율적으로 변경이 가능하다.

[ 슈팅 게임을 설계하시오 ]
유닛 종류 : 전투기, 헬리콥터
유닛들은 미사일을 발사할 수 있다.
전투기는 직선 미사일을, 헬리콥터는 유도 미사일을 발사한다.
필살기로는 폭탄이 있는데, 전투기에는 있고 헬리콥터에는 없다.

Strategy pattern을 적용한 설계는 아래와 같다.

20211120_184317

  • 상속은 무분별한 소스 중복이 일어날 수 있으므로, 컴포지션을 활용한다. (인터페이스와 로직의 클래스와의 관계를 컴포지션하고, 유닛에서 상황에 맞는 로직을 쓰게끔 유도하는 것)

  • 미사일을 쏘는 것과 폭탄을 사용하는 것을 캡슐화하자
    ShootAction과 BombAction으로 인터페이스를 선언하고, 각자 필요한 로직을 클래스로 만들어 implement한다.
  • 전투기와 헬리콥터를 묶을 Unit 추상 클래스를 만들자
    Unit에는 공통적으로 사용되는 메서드들이 들어있고, 미사일과 폭탄을 선언하기 위해 variable로 인터페이스들을 선언한다.

전투기와 헬리콥터는 Unit 클래스를 상속받고, 생성자에 맞는 로직을 정의해주면 끝난다.

  • 전투기 예시
class Fighter extends Unit {
    private ShootAction shootAction;
    private BombAction bombAction;

    public Fighter() {
        shootAction = new OneWayMissle();
        bombAction = new SpreadBomb();
    }
}

Fighter.doAttack()을 호출하면, OneWayMissle의 attack()이 호출될 것이다.

정리

이처럼 Strategy Pattern을 활용하면 로직을 독립적으로 관리하는 것이 편해진다.

로직에 들어가는 ‘행동’을 클래스로 선언하고, 인터페이스와 연결하는 방식으로 구성하는 것!


컴포지트(Composite) Pattern

목적

  • compositie pattern의 사용 목적은 object의 hierarchies를 표현하고 각각의 object를 독립적으로 동일한 인터페이스를 통해 처리할 수 있게한다.

아래 Composite pattern의 class diagram을 보자

20211120_184632

위의 그림의 Leaf 클래스와 Composite 클래스를 같은 interface로 제어하기 위해서 Component abstract 클래스를 생성하였다.

위의 그림을 코드로 표현 하였다.

Component 클래스
public class Component {
    public void operation() {
        throw new UnsupportedOperationException();
    }
    public void add(Component component) {
        throw new UnsupportedOperationException();
    }

    public void remove(Component component) {
        throw new UnsupportedOperationException();
    }

    public Component getChild(int i) {
        throw new UnsupportedOperationException();
    }
}

Leaf 클래스와 Compositie 클래스가 상속하는 Component 클래스로 Leaf 클래스에서 사용하지 않는 메소드 호출 시 exception을 발생시키게 구현하였다.

Leaf 클래스
public class Leaf extends Component {
    String name;
    public Leaf(String name) {
        ...
    }

    public void operation() {
        .. something ...
    }
}
Composite class
public class Composite extends Component {
    ArrayList components = new ArrayList();
    String name;

    public Composite(String name) {
        ....
    }

    public void operation() {
        Iterator iter = components.iterator();
        while (iter.hasNext()) {
            Component component = (Component)iter.next();
            component.operation();
        }
    }
    public void add(Component component) {
        components.add(component);
    }

    public void remove(Component component) {
        components.remove(component);
    }

    public Component getChild(int i) {
        return (Component)components.get(i);
    }
}
구현 시 고려해야할 사항
  • 위의 코드는 parent만이 child를 참조할 수 있다. 구현 이전에 child가 parent를 참조해야 하는지 고려해야 한다.

  • 어떤 클래스가 children을 관리할 것인가?

Children 관리를 위한 2가지 Composite pattern

20211120_185031

위의 예제로 Component 클래스에 add, removem getChild 같은 method가 선언이 되어있으며 Transparency를 제공한다.

  • 장점 : Leaf 클래스와 Composite 클래스를 구분할 필요없이 Component Class로 생각할 수 있다.

  • 단점 : Leaf 클래스가 chidren 관리 함수 호출 시 run time에 exception이 발생한다.

20211120_185101

이전 예제에서 children을 관리하는 함수를 Composite 클래스에 선언 되어있으며 Safety를 제공한다.

  • 장점 : Leaf 클래스가 chidren 관리 함수 호출 시 compile time에 문제를 확인할 수 있다.

  • 단점 : Leaf 클래스와 Composite 클래스를 구분하여야 한다.


관련 패턴

Decorator
  • 공통점 : composition이 재귀적으로 발생한다.

  • 차이점 : decorator 패턴은 responsibilites를 추가하는 것이 목표이지만 composite 패턴은 hierarchy를 표현하기 위해서 사용된다.

Iterator
  • 공통점 : aggregate object을 순차적으로 접근한다.

SOLID(An overview of design pattern - SOLID, GRASP)

먼저 디자인 패턴을 공부하기 전에 Design Principle인 SOLID와 GRASP에 대해서 알아보자

Design Smells

design smell이란 나쁜 디자인을 나타내는 증상같은 것이다.

아래 4가지 종류가 있다.

1. Rigidity(경직성)

시스템이 변경하기 어렵다. 하나의 변경을 위해서 다른 것들을 변경 해야할 때 경직성이 높다. 경직성이 높다면 non-critical한 문제가 발생했을 때 관리자는 개발자에게 수정을 요청하기가 두려워진다.

2. Fragility(취약성)

취약성이 높다면 시스템은 어떤 부분을 수정하였는데 관련이 없는 다른 부분에 영향을 준다. 수정사항이 관련되지 않은 부분에도 영향을 끼치기 떄문에 관리하는 비용이 커지며 시스템의 credibility 또한 잃는다.

3. Immobility(부동성)

부동성이 높다면 재사용하기 위해서 시스템을 분리해서 컴포넌트를 만드는 것이 어렵다. 주로 개발자가 이전에 구현되었던 모듈과 비슷한 기능을 하는 모듈을 만들려고 할 때 문제점을 발견한다.

4. Viscosity(점착성)

점착성은 디자인 점착성과 환경 점착성으로 나눌 수 있다.

시스템에 코드를 추가하는 것보다 핵을 추가하는 것이 더 쉽다면 디자인 점착성이 높다고 할 수 있다. 예를 들어 수정이 필요할 때 다양한 방법으로 수정할 수 있을 것이다. 어떤 것은 디자인을 유지하는 것이고 어떤 것은 그렇지 못할 것이다(핵을 추가).

환경 점착성은 개발환경이 느리고 효율적이지 못할 때 나타난다. 예를들면 컴파일 시간이 매우 길다면 큰 규모의 수정이 필요하더라도 개발자는 recompile 시간이 길기 때문에 작은 규모의 수정으로 문제를 해결할려고 할 것이다.

위의 design smell은 곧 나쁜 디자인을 의미한다.(스파게티 코드)

Robert C. Martin’s Software design principles(SOLID)

Robejt C. Martin은 5가지 Software design principles을 정의하였고 앞글자를 따서 SOLID라고 부른다.

Single Responsibility Principle(SRP)

A class should have one, and only one, reason to change

클래스는 오직 하나의 이유로 수정이 되어야 한다는 것을 의미한다.

Example SRP를 위반하는 예제로 아래 클래스 다이어그램을 보자

20211120_185545

Register 클래스가 Student 클래스에 dependency를 가지고 있는 모습이다. 만약 여기서 어떤 클래스가 Student를 다양한 방법으로 정렬을 하고 싶다면 아래와 같이 구현 할 수 있다.

20211120_185642

하지만 Register 클래스는 어떠한 변경도 일어나야하지 않지만 Student 클래스가 바뀌어서 Register 클래스가 영향을 받는다. 정렬을 위한 변경이 관련없는 Reigster 클래스에 영향을 끼쳤기 때문에 SRP를 위반한다.

20211120_185721

위의 그림은 SRP 위반을 해결하기 위한 클래스 다이어그램이다. 각각의 정렬 방식을 가진 클래스를 새로 생성하고 Client는 새로 생긴 클래스를 호출한다.

관련 측정 항목

SRP는 같은 목적으로 responsibility를 가지는 cohesion과 관련이 깊다.

Open Closed Principle(OCP)

Software entities (classes, modules, functions, etc) should be open for extension but closed for modification

자신의 확장에는 열려있고 주변의 변화에는 닫혀 있어야 하는 것을 의미한다.

Example

20211120_185824

void incAll(Employee[] emps) {
    for (int i=0; i<emps.size(); i++) {
        if(emps[i].empType == FACULTY)
            incFacultySalary((FACULTY)emps[i])
        else if(emps[i].empType == STAFF)
            incStaffSalary((STAFF)emps[i])
        else if(emps[i].empType == SECRETARY)
            incSecretarySalary((SECRETARY)emps[i])
    }
}

위의 예제는 아래 문제점을 가지고 있다.

Rigid - 새로운 employee type이 계속 요구된다.

Fragile - 많은 if/lese 구문과 코드를 찾기 어렵다

20211120_185902

이전에 설명한 문제점을 해결한 클래스 다이어그램이다.

incAll() 함수를 통해서 문제를 해결한 것을 볼 수 있다.

Liskov Substitution Principle(LSP)

subtypes must be substitutable for their base types

base 클래스에서 파생된 클래스는 base 클래스를 대체해서 사용할 수 있어야한다.

Example

아래는 Java 라이브러리의 Date 클래스이다.

20211120_185942

java.util.Date date = new java.util.Date();
int dateValue = date.getDate(); // Okay

date = new java.sql.Time(10,10,10);
dataValue = date.getDate(); // throws IllegalArgumentException
Inheritance Vs. Composition

20211120_190027

위의 예제에서 만약 List의 Implemenation을 재사용하게 된다면 inheritance보다 object composition을 사용하는 것을 추천한다.

위에서 Queue 클래스가 List 클래스를 inheritance 한다면 LSP를 위반하게 된다.

Interface Segregation Principle(ISP)

Clients should not be forced to depend on methods they do not use

사용하지 않는 메소드에 의존하면 안된다.

20211120_190115

Roast Application은 getName(), getSSN() 메소드만을 사용하고 Account Application은 getInvoice(), postPayment() 메소드만을 사용한다.

20211120_190612

위 클래스 다이어그램 처럼 클래스에 맞는 interface를 만들어서 제공하면 ISP 문제를 해결할 수 있다.

Dependency Inversion Principle(DIP)

high-level modules should not depend on low-level modules. Both should depend on abstractions

자신(high level module)보다 변하기 쉬운 모듈(low level modeul)에 의존해서는 안된다.

Inversion?

20211120_190645

Program 클래스는 Module 클래스에 dependency를 가지고 있으며 Module 클래스는 Function 클래스에 의존하고 있다.

20211120_190733

Module 클래스를 인터페이스 클래스로 변경을 한 클래스 다이어그램이다. 이전 그림과 다르게 depenedency가 inversion 된 모습을 볼 수 있다.

DIP는 dependency를 inversion 하는 것 뿐 아니라 ownership 또한 inversion 한다





© 2021.03. by yacho

Powered by github