설계 원리

BiteSnail·2023년 12월 6일
0

설계 기본 개념

요구 분석(Requirements Analysis)는 무엇(What)을 만들 것인가를 다루는 작업이라면 설계(Design)는 요구를 어떻게(How) 실현할 것인지를 결정하는 활동이라고 할 수 있습니다.

기능의 구현만을 목적으로 한다면 설계란 반쪽에 불과합니다. 기능적인 요구 뿐만 아니라, 품질 목표를 달성하기 위해서 설계과정에서는 반드시 결정이 필요하게 됩니다.

No Silver Bullet

모든 것을 만족시킬 수 있는 궁극적인해결책은 없습니다. 따라서 설계 과정에서는 품질 목표에 따라 여러 타협안을 선택하게 됩니다. 보안과 처리속도처럼 양립하기 힘든 목표에서 더 나은 선택지를 결정하는 과정에서 고민이 필요하게 되고 이를 해결하기 위해 설계 원리를 따르는 것이 중요해집니다.

시스템과 아키텍처 그리고 컴포넌트

설계 단계에서는 요구라는 목표를 이루기 위해 하나의 시스템(system)을 설계해야 합니다. 하나의 시스템을 설계하기 위해서는 시스템을 이루는 컴포넌트(component)와 컴포넌트 간의 관계가 어떻게 이루어지는 지를 결정해야 합니다.

시스템

시스템이란 어떠한 책임과 목적들을 가지고 있는 논리적인 단위입니다. 시스템은 컴포넌트들의 집합으로 구현되어 있습니다. 특히 내부의 컴포넌트가 수정되거나 다른 컴포넌트로 대체된다고 하더라도 지속적으로 존재할 수 있어야 합니다.

서브시스템(subsystem)이란 이런 시스템의 일부분이라고 할 수 있습니다. 많은 책임과 목적을 가진 시스템은 각각의 책임과 목적을 가지는 작은 시스템(=서브시스템)들의 집합으로 나타낼 수도 있습니다.

컴포넌트

컴포넌트란 명확한 역할을 가지고 있는 하나의 단위입니다. 컴포넌트는 반드시 독립적으로 존재할 수 있어야 하고 다른 컴포넌트로 대체할 수 있어야 합니다.

예를 들어 데스크탑에서 그래픽카드(이하 GPU)는 하나의 컴포넌트입니다. GPU는 데스크탑의 프레임이나 메인보드, 파워 서플라이의 용량 등과 호환된다면 기존의 것을 대체할 수 있습니다.

모듈(Module)이란 프로그래밍 언어적 단위의 컴포넌트라고 할 수 있습니다. 자바(Java)에서 사용되는 하나의 모듈 단위가 될 수도 있고, 다른 언어에서는 하나의 클래스, 심지어는 하나의 함수도 모듈 단위가 될 수 있습니다.

아키텍처

아키텍처(Architecture)란 컴포넌트들의 관계를 나타낸 것 입니다. 한 컴포넌트와 다른 컴포넌트 사이의 관계, 시스템과 시스템끼리의 관계에 대한 내용이 아키텍처라고 할 수 있습니다.

시스템은 컴포넌트들의 집합으로 하나의 시스템과 다른 시스템의 관계 또한 넓은 의미에서 컴포넌트와 컴포넌트들의 관계라고 할 수 있습니다.

설계관점

설계는 기능의 실현이 중심이 전통적인 방식, 예를 들어 상향식 설계(Bottom-up approach)와 하향식 설계(Top-down approach), 분할정복과 같은 것들이 있습니다.

최근에는 요소와 그 관계를 이루는 방식을 중심으로 생각하는 아키텍처 기반 설계를 많이 진행합니다. 아키텍처 기반 설계는 모듈, 컴포넌트, 배치(Deployment) 관점으로 나눌 수 있습니다.

설계 원리

좋은 소프트웨어를 설계하기 위해서는 설계원리에 따라 진행하는 것이 좋습니다. 설계 원리는 전통적인 설계원리와 그 설계원리들이 객체지향 프로그래밍과 함께 발전하여 추가적으로 5가지의 객체지향 설계 원리 SOLID가 등장하게 되었습니다.

전통적인 설계 원리

전통적인 설계 원리는 공간, 시간적 효율성(efficiency)과 시스템이 단순하고 이해하기 쉬워서 유지보수에 이점이 있도록 하는 단순성(simplicity)을 높이는 것을 중요하게 생각하였습니다. 이 외에도 추상화, 캡슐화, 모듈화의 원리를 적용하여 시스템을 설계해야 합니다.

추상화, 캡슐화, 모듈화

필자는 추상화(Abstraction)가 소프트웨어 개발에서 가장 중요한 개념이라고 생각합니다. 추상화란 간단하게 어떤 객체(Object)에 대해서 특정한 목적에 관련된 정보에만 집중하는 것을 의미합니다. 이를 통해 데이터의 복잡성을 줄이고 시스템을 효율적으로 구성할 수 있게 합니다.

캡슐화(encapsulation)란 서비스를 수행하는 핵심 요소만을 제공하고 나머지는 숨기는 것을 의미합니다. 캡슐화를 통해 보안 뿐만 아니라 내부 구조에 대한 고민이 없어지기 때문에 변경에 용이하게 대응할 수 있습니다.

모듈화(Modularization)이란 문제를 구성요소(=컴포넌트)가 될 만한 수준으로 분할하는 것을 말합니다. 하나의 소프트웨어를 여러 개의 작은 패키지나 클래스 등으로 나누면 변경 사항이 있을 때 일부 모듈들의 수정으로 해결할 수 있게 됩니다. 또한 작은 기능들은 테스트하기에 용이해진다는 장점도 있습니다.

응집

응집(cohesion)이란 서브시스템이나 모듈 안에서 수행되는 작업들이 서로 관련된 정도를 의미합니다. 높은 수준의 응집이란 여러 요소들이 하나의 목적을 위하여 유기적으로 관련되어 있음을 의미합니다.

어떤 모듈을 정의할 때 기능을 한 마디로 요약할 수 없다면 여러 가지 기능이 복합되어 있다고 말할 수 있습니다. 이럴 때 응집도가 낮아진다고 할 수 있습니다.

  • 정보적 응집(Informational)
    • 모든 작업들이 같은 데이터에 대해 실행되는 응집을 의미합니다. 이는 어려운 것이 아닙니다. 일례로 어떤 객체에 대한 클래스가 해당할 수 있습니다.

      class Student extends Person{
      	private String firstName;
      	private String lastName;
      	private String id;
      	public void setFirstName(){...}
      	public void getFullName(){...}
      	public void sayHello(){...}
      }
  • 기능적 응집(functional)
    • 단일 작업을 수행하여 결과를 내도록 하는 것들의 응집입니다. 기능을 수행하면서 다른 행위(side effect)가 발생하지 않아야 합니다.

      class Trigonometry{
      	public float sin(float angle){...}
      	public float cos(float angle){...}
      	public float tan(float angle){...}
      }
  • 계층적 응집(hierarchical)
    • 통신 프로토콜에서도 찾아볼 수 있으며, 다른 층에 영향을 주지 않고 대체할 수 있는 것들의 응집입니다. TCP와 UDP는 전송계층에서 서로 대체할 수 있는 프로토콜입니다.

      class TCP extends TransportLayer{
      	...
      	public void send(byte[] buffer){...}
      	public void receive(byte[] buffer){...}
      }
      
      class UDP extends TransportLayer{
      	...
      	public void send(byte[] buffer){...}
      	public void receive(byte[] buffer){...}
      }
  • 교환적 응집(communicational)
    • 동일한 데이터에 대해 동작하는 것들의 응집입니다. 예를 들어 특정 데이터를 저장하고 출력하는 기능을 가진 모듈은 교환적 응집을 가진다고 할 수 있습니다.

      class DataManager extends Manager{
      	//아래 메서드는 어떤 같은 데이터에 대해 저장과 출력이라는 기능을 수행합니다.
      	//이는 교환적 응집을 가진다고 할 수 있습니다.
      	public saveAndPrint(){...}
      }
  • 순차적 응집(sequential)
    • 순차적 응집은 함수형 프로그래밍 언어에서 자연스럽게 발생하며 어떤 출력물이 다른 기능의 입력으로 사용될 때 순차적 응집을 가진다고 할 수 있습니다. 아래는 텍스트 파일을 읽어 처리과정을 거친 후 다른 파일에 저장하는 자바 코드입니다.

      class SequentialClass{
      	public String readData(String readPath){...}
      	public String processData(String data){...}
      	public void saveData(String processedData, savePath){...}
      	public void saveDataAfterProcess(readPath, savePath):
      	    // Step 1: Read data
      	    String data = readData(readPath);
      	    // Step 2: Process data
      	    String processedData = processData(data);
      	    // Step 3: Save data
      	    saveData(processedData, savePath);
      }
  • 절차적 응집(procedural)
    • 절차적 응집은 모듈 안에서 수행되는 연산이 프로그램에서 수행되는 순서와 관련이 있는 것을 의미합니다. 순차적 응집과 유사하지만 절차적 응집은 반드시 이전 결과를 다음 입력으로 사용하지 않아도 된다는 차이점이 존재합니다.

      class TenisGame{
      	public void resultProcess(){
      		showScore();
      		showWinner();
      		goMenu();
      	}
      }
  • 시간적 응집(temporal)
    • 시간적 응집들은 프로그램의 수행에서 같은 단계에서 수행되는 작업들이 모여 있는 것들을 모아놓은 것을 의미합니다.

      class TemporalClass{
      	public void init(){
      		//각 작업들은 init 단계에서 수행되지만 어떤것이 우선이되든 상관 없음
      		checkDBConnection();
      		checkInternetConnection();
      		checkFileSystem();
      		checkTimeSytem();
      		checkIOSystem();
      	}
      }
  • 논리적 응집(logical)
    • 논리적 응집이란 논리적으로 관련은 있지만 기능적인 부분에서는 관련이 없는 것들의 모임이라고 할 수 있습니다. 예를 들어 키보드 입력을 처리하는 부분과 마우스 입력을 처리하는 부분이 한 모듈에 존재한다면 논리적 응집이라고 할 수 있습니다.

      class Reader{
      	public KeyCode readMouse(){...}
      	public KeyCode readKeyboard(){...}
      }

결합

결합(coupling)은 모듈 간에 의존하는 정도를 뜻합니다. 모듈들 간의 의존이 높아지면, 즉 결합이 강해지면 시스템은 이해하기 어려워지고 하나의 변경에 대한 작업량이 많아집니다. 따라서 좋은 소프트웨어란 결합력이 낮아야 합니다. 결합은 완벽하게 제거할 수 없기 때문에 상황에 따라 적절한 정도로 제거하는 것이 좋습니다.

  • 내용 결합(content coupling)
    • 한 모듈이 다른 모듈을 직접 참조하는 것을 의미합니다. 한 모듈이 다른 모듈을 직접 참조하게 되면 참조하는 특정 데이터가 변경되었을 경우 결합된 모든 모듈들이 이를 수정해야 합니다.

      아래는 StudentManager 클래스에서 Student의 멤버변수를 바로 수정하고 있으며 내용 결합에 해당합니다. Student 클래스에서는 변수를 캡슐화하여 다른 클래스에서 특정 메소드를 통해서만 값을 수정하도록 해야 합니다.

      class Student{
      	public int score;
      }
      class StudentManager{
      	public void setScores(Student[] students){
      		foreach(Student student in stduents){
      			student.score = 0;
      		}
      	}
      }
      class Student{
      	private int score;
      	public void setScore(int score);
      	public void setScore(String score);
      }
      class StudentManager{
      	public void setScores(Student[] students){
      		foreach(Student student in stduents){
      			student.setScore("0");
      		}
      	}
      }
  • 공통 결합(common coupling)
    • 한 모듈이 다른 모듈이 읽은 전역 변수 값을 쓰거나 변경하는 것을 의미합니다. 전역변수를 사용하는 모든 모듈들에 발생하는 결합으로 피할 수 없는 결합입니다.

      공통 결합의 경우 싱글톤패턴(singleton pattern)을 적용해 어느 정도 결합을 낮출 수 있습니다. 완벽한 제거는 불가능합니다.

      class GameManager{
      	private static GameManager instance = new GameManager();
      	private GameManager(){...} //private 생성자(외부 생성 불가능)
      	public static void start(){...}
      	public static void pause(){...}
      	public static void resume(){...}
      	public static void end(){...}
      }
  • 외부 결합(External coupling)
    • 외부에서 정의된 운영체제나 공유 라이브러리, 하드웨어 등에 의존하는 경우에 외부 결합이 발생합니다. 이는 외부 환경이 변화 시 결합된 모듈들을 수정해야 함을 의미합니다.

      코드에 의존성을 가질 수 있는 부분을 줄이는 것이 최선의 방법이며 퍼싸드(facade) 패턴을 통해 외부 결합을 최소화할 수 있습니다. 자바에서는 JDBC라는 프로그래밍 인터페이스를 통해 데이터베이스와 통신합니다. 이를 통해 DBMS에 종속되지 않는 소프트웨어 개발이 가능합니다.

      class UserRepository{
      	JDBC myDatabase;
      	public UserRepository(JDBC database){...}
      	public void save(User user){...}
      	public void findAll(){...}
      }
  • 제어 결합(control coupling)
    • 제어 결합이란 제어문을 통해 서로 다른 기능을 호출할 때 발생합니다. 다형성을 통해 제거할 수 있습니다.

      예를 들어 다음과 같이 Painter라는 클래스에서 원을 그리는 메서드와 사각형을 그리는 메서드가 존재합니다. 이때 매개변수에 따라 다른 작업을 수행하는 메서드가 존재할 경우 코드의 복잡성을 증가시킬 수 있습니다.

      class Painter{
      	private drawCircle(){...}
      	private drawRectangle(){...}
      	public drawFigure(boolean isCircle){
      		if (isCircle){
      			drawCircle();
      		}
      		else {
      			drawRectangle();
      		}
      	}
      }

      이런 경우 상속이나 인터페이스를 이용해 하나의 메서드로 동작할 수 있도록 해야 합니다.

      class Circle implements Drawable{
      	...
      	public draw(){...}
      }
      class Rectangle implements Drawable{
      	...
      	public draw(){...}
      }
      
      class Painter{
      	public drawFigure(Drawable figure){
      		figure.draw();
      	}
      }
  • 스탬프 결합(stamp coupling)
    • 복합 데이터 구조의 일부를 사용하는 모듈에 복합 데이터 구조를 전달하는 것을 의미합니다.

      예를 들어 이메일을 보내는 작업에서 User객체의 name과 email만 필요하다면 아래 코드처럼 객체를 보낼 경우 프로그래머의 실수로 인해 사이드이펙트가 발생할 가능성이 있습니다.

      public class MailSender{
      	public void sendMail(User user, String text){...}
      }

      이를 해결하기 위해 복합 데이터 매개변수를 단순 변수들로 변경하거나 인터페이스를 매개변수로 사용할 수 있습니다.

      public class MailSender{
      	public void sendMail(String name, String email, String text){...}
      }
  • 데이터 결합(data coupling)
    • 모듈들이 주고받는 매개변수가 기본타입들인 경우를 의미합니다.

      예를 들어 어떤 기능을 수행하기 위해 필요한 매개변수가 매우 많을 경우에 발생할 수 있습니다. 이런 경우 적절하게 DTO(Data Transfer Object)를 만들어서 전달하는 것도 하나의 해결방법이 될수 있습니다.

      public class Checker{
      	public checkOperation(int a, String b, float c, double d, ..., String z){...}
      }
      public class Checker{
      	public checkOperation(CheckDTO checkInfomation){...}
      }

스탬프 결합과 데이터 결합은 서로 양립할 수 없는 결합으로 하나를 가지면 자연스럽게 하나는 놓칠 수 밖에 없습니다. 따라서 개발하려는 소프트웨어의 특성에 맞게 적당한 결합을 선택해야 합니다.

  • 루틴 호출 결합(call coupling)
    • 루틴 호출 결합이란 하나의 루틴(=메서드)에서 다른 루틴을 호출할 때 발생합니다. 이는 완전한 제거가 불가능한 결합으로 줄이기 위해서는 반복되는 호출을 결합하여 하나의 루틴으로 만들어야 합니다.

      아래 예제에서는 walk와 run이 반복해서 사용되고 있는데, 이들을 결합하여 하나의 호출로 만들어 결합도를 낮출 수 있습니다.

      public class Person{
      	public void walk(){...}
      	public void run(){...}
      }
      
      public class Trainer{
      	public void interval(Person person, int times){
      		for(int i=0;i<times;i++){
      			person.walk();
      			person.run();
      		}		
      	}
      }
      public class Person implements IntervalRoutine{
      	public void walk(){...}
      	public void run(){...}
      	public void routine(){...}
      }
      
      public class Trainer{
      	public void interval(IntervalRoutine person, int times){
      		for(int i=0;i<times;i++){
      			person.routine();
      		}		
      	}
      }
  • 타입 사용 결합(type use coupling)
    • 타입 사용 결합은 한 모듈이 다른 모듈에서 정의된 데이터 타입을 사용하는 경우에 발생합니다. 한 자료형을 바꾸었을 때 다른 모듈에서 받는 영향을 최소하도록 하는 것이 결합도를 낮출 수 있는 방법입니다.

      아래와 같이 정의된 enum 클래스에 요일 비교나 추가 생성자를 이용할 수 있습니다. 이를 통해 타입을 사용하는 클래스에서 수정에 대한 영향을 최소화할 수 있도록 할 수 있습니다.

      
      public enum DayOfWeek{
      	MONDAY,
      	TUESDAY,
      	WEDNESDAY,
      	THURSDAY,
      	FRIDAY,
      	SATURDAY,
      	SUNDAY
      	//추가 생성자를 추가하여 영향 최소화
      	public static DayOfWeek of(LocalDate date){...}
      	//요일 비교에 대한 메서드를 추가하여 영향 최소화
      	public static int secondUntilWeekend(LocalDate date){...}
      }

기타 설계 원리

  • 재사용성 증진
    • 다른 시스템에서 다시 사용할 수 있도록 설계합니다.
  • 유연성 고려
    • 미래에 수정될 수도 있음을 고려해 설계합니다.
  • 노후 예측
    • 비교적 안정화 된 프레임워크나 툴을 사용하도록 설계합니다.
  • 이식성
    • 가능하다면 많은 플랫폼에서 사용할 수 있도록 합니다.
  • 테스트 가능성
    • 기능별로 테스트가 용이하도록 설계합니다.
  • 방어적인 설계
    • 부적절한 사용에 대한 방어적인 설계가 필요합니다.

객체지향 설계 원리

객체지향 패러다임의 발전으로 인해 상속이나 인터페이스 등과 같은 구문들이 추가되었습니다. 이를 통해 기존 설계원리들을 만족시킬 수 있도록 Martin은 SOLID라고 불리는 5가지 원칙을 제시하였습니다.

SOLID를 제안한 Robert C. Martin - 출처: Wikipedia
SOLID를 제안한 Robert C. Martin - 출처: Wikipedia

SOLID

SOLID는 상속과 인터페이스와 같은 구문들을 사용하여 유지보수와 확장이 쉬운 시스템을 만들 수 있는 다섯가지 설계 기본 원칙입니다.

소스 코드가 기본 원칙을 만족하지 않는 경우 코드 스멜(code smell)을 가진다고 얘기하고 설계 원칙에 근거한 리팩토링(refactoring)을 통해 이를 제거할 수 있습니다.

S: 단일 책임의 원리

단일 책임의 원리(Single Responsibility Principle)는 한 클래스가 하나의 책임만 가져야 한다라는 의미를 가집니다.

예를 들어 책을 나타내는 Book이란 클래스를 살펴보겠습니다. getter나 setter와 같이 내부 멤버 변수를 참조하는 메서드 뿐만 아니라 isIn과 같이 논리적으로 Book이라는 클래스가 수행할 수 있는 메서드가 존재합니다.

그런데 showTextToWindow 메서드 같은 경우에는 Book 클래스가 text를 어떤 화면에 보여주기 위한 책임을 추가합니다. 이는 단일 책임의 원리에 위배되며 이러한 메서드는 화면을 출력하는 책임을 가진 다른 클래스를 만들어 해결할 수 있습니다.

class Book{
	private String name;
	private String author;
	private String text;
	public String getName();
	public String setAuthor();
	public boolean isIn(String word);
	public void showTextToWindow();
}
class Book{
	private String name;
	private String author;
	private String text;
	public String getName();
	public String setAuthor();
	public boolean isIn(String word);	
}

class Window{
	public void showTextToWindow(String text);
}

O: 개방 폐쇄 원칙

개방 폐쇄 원칙(Open/Closed Principle)은 소프트웨어 요소는 확장에 열려 있으나 변경에는 닫혀 있어야 한다는 의미입니다.

이는 기능의 확장에는 열려 있어야 하며 이 기능의 확장 때문에 기존 코드를 수정해야 하는 것을 방지해야 한다는 의미입니다.

예를 들어 다양한 정렬 방법을 적용하기 위해서 정렬 방법이 추가될 때마다 기존 코드를 수정해야 하는 것은 개방 폐쇄 원칙에 위배된다고 할 수 있습니다. 아래 예제에서는 SortAlgorithm에서 name에 따라 다른 정렬을 수행하도록 하였습니다.

class SortAlgorithm<T>{
	private String name;
	private void bubbleSort(T[] array){...}
	private void heapSort(T[] array){...}
	private void shellSort(T[] array){...}
	public SortAlgorithm(String name){...}
	//기능의 추가에 대한 수정이 필요하여 원칙 만족 못함
	public void sortArray(T[] array){
		if(name.equals("bubble")){
			bubbleSort(array);	
		}
		else if(name.equals("heap")){
			heapSort(array);
		}
		...
	}
}

class Client{
	private int[] initRandomArrs(){...}
	public static void main(String args[]){
			SortAlgorithm<int> bubbleSort = new SortAlgorithm<>("bubble");
			SortAlgorithm<int> heapSort = new SortAlgorithm<>("heap");
			int[] arrs = initRadomArrs();
			bubbleSort.sortArray(arrs);
			heapSort.sortArray(arrs);
	}
}

만약 다른 정렬 방법이 추가되어야 한다면 프로그래머는 SortAlgorithm 클래스 내에 또 다른 if 분기문을 만들어주고 이에 대한 비즈니스 로직을 추가해야 합니다.

이를 해결하기 위해 공통적인 로직을 인터페이스로 묶어 개방 폐쇄 원칙을 만족시킬 수 있습니다.

interface Sortable<T>{
	public void sort(T[] array);
}

class BubbleSort<T> implements Sortable<T>{
	public void sort(T[] array){...}
}
class HeapSort<T> implements Sortable<T>{
	public void sort(T[] array){...}
}
class ShellSort<T> implements Sortable<T>{
	public void sort(T[] array){...}
}

class SortAlgorithm<T>{
	//새로운 정렬 방법이 추가되도 수정할 필요 없음.
	public void sortArray(Sortable<T> type, T[] array){
		type.sort(array);
	}
}

class Client{
	private int[] initRandomArrs(){...}
	public static void main(String args[]){
			SortAlgorithm<int> sortAlgorithm = new SortAlgorithm<>();
			int[] arrs = initRadomArrs();
			sortAlgorithm.sortArray(new BubbleSort<int>(), array);
			sortAlgorithm.sortArray(new HeapSort<int>(), array);
	}
}

L: 리스코프 치환 원칙

리스코프 치환 원칙(Liskov substitution Principle)은 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다 라는 의미를 가집니다.

좀 더 쉽게 얘기하자면 아래의 표현이 정확한 것 같습니다.

LSP는 한마디로 다형성을 지원하기 위한 원칙 이라고 딱 잘라 정의할 수 있다.

출처:https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-LSP-리스코프-치환-원칙

다형성을 만족한다는 것은 부모 클래스의 기능을 자식 클래스가 해치지 않아야 한다는 것입니다. 예를 들어 Animal이라는 클래스에 walk라는 메서드가 존재할 경우를 가정해 봅니다. 대부분의 동물들은 해당 메서드를 수행할 수 있겠지만 일부 동물들은 walk가 가능하지 않을 수 있습니다.

class Animal{
	public void walk(){...}
}

class Cow extends Animal{
	public void walk(){...}
}

class Dog extends Animal{
	public void walk(){...}
}

class Snake extends Animal{
	public void walk(){
		//뱀이 걸을 수가 있나?
	}
}

개발자는 Snake 클래스에 대해 walk라는 메서드를 구현하지 않거나 다른 방식으로 구현하게 됩니다.

컴파일 단계에서는 문제가 없지만, 만약 Snake를 업캐스팅하여 walk 메서드를 수행했을 때 어떠한 문제가 발생할지 알 수 없습니다. 따라서 자식 클래스가 부모 클래스의 기능을 대체하지 못하고, 리스코프 치환 원칙을 위배하게 됩니다.

이런 경우 상속 관계를 다시 정의하거나 인터페이스를 통해 해당 메서드를 분리함으로써 위반 상황을 막을 수 있습니다.

class Animal{
}

interface Walkable{
	public void walk();
}

class Cow extends Animal implements Walkable{
	public void walk(){...}
}

class Dog extends Animal implements Walkable{
	public void walk(){...}
}

class Snake extends Animal{
}

I: 인터페이스 분리 원칙

인터페이스 분리 원칙(Interface Segregation Principle)은 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다 라는 의미를 가집니다.

인터페이스에 너무 많은 기능을 추가하면 이를 구현해야 하는 클래스에서는 실제로 사용하지 않는 인터페이스까지 구현해야 하는 문제가 발생합니다.

interface Animal{
	public void fly();
	public void walk();
	public void swim();
}

//people은 fly 불가능..
class People implements Animal{...}

//Salmon는 fly와 walk 불가능..
class Salmon implements Animal{...}

따라서 인터페이스를 적절히 분리하여 클래스가 사용해야 하는 기능만을 구현할 수 있도록 해야 합니다.

interface Swimmable{
	public void swim();
}

interface Flyable{
	public void fly();
}

interface Walkable{
	public void walk();
}

class People implements Swimmable, Walkable{...}
class Salmon implements Swimmable{...}

D: 의존관계 역전 원칙

의존관계 역전 원칙(Dependency Inversion Principle)은 프로그래머가 추상화에 의존해야지, 구체화에 의존하면 안된다. 라는 의미를 가집니다.

이는 특정 기능을 수행할 때 구체화 된 클래스에 의존하는 것이 아닌, 추상화 된 인터페이스를 의존하는 것을 의미합니다.

예를 들어 게임에서 탈것을 구현한다고 했을 때 구체화 된 클래스를 통해 구현하게 된다면 추후 확장성에 제약이 생길 수 있습니다.

class Horse{
	public void mount(){...}
	public void unmount(){...}
}

//탈것이 바뀌면 모두 바꾸어야 함.
class Mount{
	private Horse horse;
	public Mount(Horse horse){
		this.horse = horse;
	}
	public void mount(){
		horse.mount();
	}
	public void unmount(){
		horse.unmount();
	}
}

이를 방지하기 위해서 인터페이스를 의존하게 만들어 확장이 가능하도록 만듭니다.

interface Mountable{
	public void mount();
	public void unmount();
}

class Horse implements Mountable{
	public void mount(){...}
	public void unmount(){...}
}

class Car implements Mountable{
	public void mount(){...}
	public void unmount(){...}
}

class Mount{
	//Mountable만 바뀌면 계속 유지 가능
	private Mountable object;
	public Mount(Moutable object){
		this.object = object;
	}
	public void mount(){
		object.mount();
	}![](https://velog.velcdn.com/images/bitesnail/post/e45f988b-2279-463e-a7d3-471da9faa39b/image.png)

	public void unmount(){
		object.unmount();
	}
}

설계안 결정

목표와 우선순위를 통해 설계안을 결정합니다. 설계 옵션 중에서 이식성이나 보안, 유지보수성 같은 목표들을 얼마나 만족시킬 수 있는지 찾고, 우선순위를 통해 가장 이상적인 설계안을 찾습니다.

설계는 중요한 부분이니만큼 많은 고민을 통해 합리적으로 선택해야 합니다.

profile
느리지만 조금씩

0개의 댓글

관련 채용 정보