설계 원칙 5가지


  1. 단일 책임 원칙 (Single Responsibility principle : SRP)
  2. 개방-폐쇄 원칙 (Open-closed principle : OCP)
  3. 리스코프 치환 원칙 (Liskov substitution principle : LSP)
  4. 인터페이스 분리 원칙 (Interface segregation principle : ISP)
  5. 의존 역전 원칙 (Dependency Inversion principle : DIP)

1. 단일 책임 원칙 (SRP: Single responsibility principle)


정의 : 클래스는 단 한개의 책임을 가져야 한다. ( = 클래스를 변경하는 이유는 단 한 개여야 한다 )

예시) 단일 책임 법칙 위반

  • 현상 : 하기 코드는 데이터를 읽는 책임 + 화면에 보여주는 책임 두개를 모두 가지고 있고, 읽은 데이터를 업데이트하여 보여주기 때문에 결합도가 매우 높다.
    • 문제점
      1. 데이터를 읽는 책임에서 읽는 프로토콜이 달라지는 경우 ( HttpClient → Socket ) 리턴 타입도 달라지게 되어 해당 클래스 전체적으로 수정이 필요하다.
      2. 책임이 분리되어 있지 않기 때문에 필요하지 않은 패키지까지 필요하다 ( ? 재사용이 어렵다 ? )

SRP_violation

  • 개선 방법
    1. 두 개의 책임을 분리 (DataViewer → DataLoader, DataDisplayer)
    2. 프로토콜에 따라 달라지는 데이터 타입을 추상화 (String or byte[] → Data)
      • 효과
      1. 데이터 읽어오는 책임에 변경이 생겼을 때 데이터를 보여주는 책임은 변경되지 않는다.
      2. 데이터를 보여주는 책임이 변경되어도 읽어오는 책임에는 변경이 없다. (테이블 보여주기 → 그래프 보여주기 변경) - 개선 팁
        • 메서드를 실행하는 주체가 누구인지 확인한다.

        → 위 예제에서 데이터 읽어오는 loadHtml호출 주체도 DataViewer이고, updateGui 메서드를 통해 화면에 보여주는 부분을 호출하는 주체도 DataViewer이다.

SRP_applied

2. 개방 폐쇄 원칙 (OCP : Open-closed principle)


확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.

→ 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다.

→ 확장되는 부분이 추상화되면 가능하다.

1번에서 SRP 로 개선한 예에 OCP를 더해봄.

SRP+OCP

OCP 원칙이 깨질 때 주요 증상

  • 추상화 + 다형성을 이용하여 개방 폐쇄 원칙을 구현하기 때문에 추상화와 다형성이 제대로 지켜지지 않은 코드는 OCP 원칙을 어기기 쉽고 다음과 같은 특징을 가진다.
    1. 다운캐스팅
    2. 비슷한 if-else 블록

OCP_violation

3. 리스코프치환의 원칙 (LSP : Liskov substitution principle)


추상화 + 다형성을 이용하여 구현되는 개방 폐쇄의 원칙을 받쳐주는 다형성에 관한 원칙을 제공해 주는 역할로

상위 타입의 객체를 하위타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.

LSP

리스코프 치환의 원칙을 지키지 않았을 때의 문제

직사각형 - 정사각형 문제 : 문제의 핵심은 정사각형이 직사각형의 특수한 형태로 보고 구현하였을 때이고 예제는 아래와 같다.

public class Rectangle {
	int width;
  int height; 

  public void setWidth(int width){
	  this.width = width;
	}

	public void setHeight(int height){
	  this.height = height;
	}
}
public class Square extends Rectangle {
	@Override
	public void setWidth(int width){
		super.setWidth(width);
    super.setHeight(wdith);
	}

	@Override
	public void setHeight(int height){
		super.setWidth(height);
		super.setHeight(height)
	}
}
publid void increaseHeight(Rectangle rec){
		if(rec.getHeight() <= rec.getWidth()){
			rec.setHeight(rec.getWidth() + 10);
		}
}
public void increaseHeight(Rectangle rec){
	if(rec instanceof Square)
		throw new CantSupportSqaureException();
	
	if(rec.getHeight() <= rec.getWidth()){
			rec.setHeight(rec.getWidth() + 10);
		}
}

문제 근원 1. 클라이언트의 입장에서 모델을 검정해야하는데 프로그래머의 입장에서 적절하다고 판단되는 가정을 하고 Base class를 만들었기 때문이다.

LSP_violation_problem_1

문제의 근원 2.

LSP 원칙이 IS-A 관계에서 ‘행동’에 초점을 맞추지만 이 행동이 내제된 고유의 행동이 아니라 (가로 세로 길이 정하기)

행동에 의한 결과가 클라이언트의 입장에서 보았을때 맞는 결과를 도출해 내는 행동인지가 중요

(가로 세로 길이 정하기라는 행동이 정사각형과 직사각형에게 동일하게 적용시 다른 결과 도출)

LSP_violation_problem_2

문제의 근원 3.

LSP는 Design By Condition(DbC)컨셉과 밀접한 관련이 있는데 DbC는 PreCondition, PostCondition 이라는 것이 있고, PreCondition은 메서드가 실행 전 조건들이 모두 참이여야하고, PostCondition은 메서드 실행 후 조건들이 모두 참이여야 한다고 함.

Rectangle행동을 기대한 아래 메서드는 자식 타입 Sqaure를 받게되면 PostCondition이 깨지게 된다.

set(new Sqaure());
...

public void set(Rectangle r){
	r.setWidth(5);
  r.setHeight(4);
  assert r.getWidth() * r.getHeight() == 20;
}

Robert C. Martin, The Liskov Substitution Principle, C++ Report, March 1996.

https://web.archive.org/web/20150905081111/http://www.objectmentor.com/resources/articles/lsp.pdf

→ 이분의 결론은 Object Oriented Design에서는 OCP 가 핵심이고 OCP를 고수하는 프로그래밍에서 LSP가 중요한 역할을 한다고 한다.

Design By Contract

https://objectcomputing.com/resources/publications/sett/september-2011-design-by-contract-in-java-with-google#:~:text=Design by Contract™ is,values into the code itself.

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


인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

클라이언트는 본인이 사용하지 않은 인터페이스를 사용하도록 요구 받지 않아야 한다.

ex. helpLoadData를 SocketDataLoader에서 사용하는 것이 불필요하니 사용하지 않고 필요한 곳에서만 사용할 수 있도록 해준다.

ISP

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


고수준 모듈은 저수준 모듈의 구현의 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야한다.

고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈

저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능을 실제 구현으로 정의

DIP

고수준 모듈이 저수준 모듈에 의존할 때의 문제

  • 프로젝트 초기에 요구 사항이 어느 정도 안정화 되면 이후 부터는 큰틀에서 프로그램이 변경되기 보다는 상세 수준에서 변경할 가능성이 높아진다.
  • 최초에 DataLoader에서 모든 기능을 다 적어놓은경우 변경이 생기면 if else 등을 사용하여 변경하다보면 복잡해지고 휴먼에러로 이어질 가능성도 있지만 고수준/저수준으로 분리하고 상세 기능을 저수준에서 작성하면 Main과 Main에서 사용하는 DataLoader 상위 모듈에는 변경이 없어지게되고, 적은 리스크로 빠른 요구 사항을 반영할 수 있다.

DIP_violation

소스 코드 의존과 런타임 의존

DIP_code

소스코드의존

DIP_compile_time

런타임 의존

DIP_runtime

의존 역전의 원칙이 소스코드 상에서 의존을 역전 시키는 원칙인데, 다형성이 있기 때문에 가능하다고 본다.

https://www.geeksforgeeks.org/difference-between-compile-time-and-run-time-polymorphism-in-java/#:~:text=Method overloading is the compile,but associated in different classes.

DIP_compile_runtime_comparison

의존 역전과 패키지

  • 의존 역전 원칙은 타입의 소유도 역전시킨다. 타입 소유 역전은 각 패키지를 독립적으로 배포할 수 있도록 해준다.

DIP_package

DI (Dependency Injection)


DI

IoC (Inversion of Control)

오브젝트의 생성, 생명주기 관리, 메서드 수행 등을 개발자가 직접하는 것이 아니라 제 3자가 수행하는 것으로 위에 예에서는 IoC Container가 되겠다. (Spring framework)

DI (Dependency Injection)

IoC 원칙을 고수한 디자인 패턴 중 하나로 오브젝트간에 연결을 오브젝트가 직접하는 것이 아니라 assembler의 역할을 하는 클래스를 통해 하게 되고 Injector라고도 부른다.

의존을 주입하는 방법 3가지

  • Constructor Injection
  • Property Injection
  • Method Injection

DI_diagram DI_code

참고

https://medium.com/nmc-techblog/dip-ioc-di-know-them-better-abed5b57fd20

https://www.tutorialsteacher.com/ioc/introduction