본문 바로가기

Study/Lotte Study

SOLID

객체 지향적 설계가 굉장히 중요하다.
그만큼 어려운 과정!

객체 지향 설계 과정

  1. 요구사항(기능)을 찾고 세분화한다. 그 기능을 알맞은 객체로 할당한다.
  2. 기능을 구현하는 데에 필요한 데이터를 객체에 추가한다.
  3. 해당 데이터를 이용하는 기능을 구현한다. (기능은 캡슐화)
  4. 객체 간 어떻게 메소드 호출 주고받을 지 결정한다.

 

SOLID 의 궁극적인 목표는 '변경에 유연해야 한다'는 것. 아키텍처는 형태에 독립적이어야 하고 그럴 수록 실용적인 아키텍처가 된다.  로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍  설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.

 

SRP (Single Responsibility Principle)

단일 책임 원칙. 소프트웨어에서 하나의 모듈은 오직 하나의 액터만 책임져야 한다. 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성이 훨씬 더 높아진다. 이 문제를 해결하기 위해 서로 다른 액터를 뒷받침하는 코드를 분리해야 한다. 즉 하나의 기능만을 가지고 있어야 한다. 

class user {
	private Database db;
	private String name;
	private Date birth;
    
	public user(String name, Date birth){
		this.db = Database.connect();
	}
    
	public String getUser(){
		return this.name + "(" + this.birth + ")";
	}
    
	public void save(){
		..
	}
}

↑ SRP가 지켜지지 않음

class User{

	// 생성자
	public User(String name, Date birth){}
    
	public void getUser(){
		return this.name + "(" + this.birth + ")";
	}
}

class UserRepository{
	private Database db;
    
	public UserRepository(){
		this.db = Database.connect();
	}
    
	public void save(User user){
		...
	}
}

↑ user 클래스는 데이터 모델과 관련된 속성을 정의하는 책임만 지게 되므로 SRP 만족

장점

  • 응집도가 높고 결합도가 낮아진다.
  • 클래스가 여러 기능을 맡게 되면 클래스 내부 함수끼리 강한 결합을 발생할 가능성이 높아지는데 이는 유지보수 비용이 증가하게 된다.

 

OCP (Open-closed Principle)

개방 폐쇄 원칙. 소프트웨어 개체는 확장에 열려 있고 변경에는 닫혀 있어야 한다. 이를 위해 자주 사용되는 문법이 인터페이스이다.

class Card {
	private String code;
	private Date expiration;
	protected int monthlyCost;
    
	public Card(String code, Date expiration, int monthlyCost) {
		this.code = code;
		this.expiration = expiration;
		this.monthlyCost = monthlyCost;
	}
    
	public String getCode(){
		return this.code;
	}
    
	public int monthlyDiscount(){
		return this.monthlyCost * 0.02;
	}
    
	public Date getExpiration(){
		return this.expiration;
	}
}

class GoldCard extends Card {
	public int monthlyDiscount() {
    	return this.monthlyCost * 0.05;
    }
}

class SilverCard extends Card {
	public int monthlyDiscount() {
    	return this.monthlyCost * 0.03;
    }
}

 

LSP (Liskov Substitution Principle)

리스코프 치환 원칙. MIT 컴퓨터 사이언스 교수인 리스코프가 제안한 설계 원칙. 상호 대체 가능한 구성요소를 이용해 소프트웨어를 만들 수 있으려면 이들 구성 요소는 반드시 서로 치환 가능해야 한다. 즉 부모 클래스와 자식 클래스 사이의 행위에는 일관성이 있어야 한다는 원칙이며 이는 객체 지향 프로그래밍에서 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용해도 문제가 없어야 한다는 것을 의미한다.

부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙. 객체 지향 언어에서는 객체의 상속이 일어나는데 이 과정에서 부모/자식 관계가 정의된다. 자식 객체는 부모 객체의 특성을 가지며 이를 토대로 확장할 수 있다. 이 과정에서 무리하거나 객체의 의의와 어긋나는 확장으로 인해 잘못된 방향으로 상속되는 경우가 생긴다. 이 원칙은 올바른 상속을 위해 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙이다.

예제 )

public class Rectangle{
	protected int width;
	protected int height;

	public int getWidth(){return width;}
    
	public int getHeight(){return height;}
    
	public void setWidth(int width){this.width = width;}
    
	public void setHeight(int height){this.height = height;}
    
	public int getArea(){return width * height;}
}

직사각형 계산하는 클래스

public class Square extends Rectangle{
	public void setWidth(int width){
		super.setWidth(width);
		super.setHeight(getWidth());
	}
    
	public void setHeight(int height){
		super.setHeight(height);
		super.setWidth(getHeight());
	}
}

정사각형 계산하는 클래스. Rectangle 클래스를 상속받아 입력받은 height와 width로 지정하여 계산한다.

public static void main(String[] args){
	Rectangle rectangle = new Square();
	rectangle.setWidth(10);
	rectangle.setHeight(5);
	System.out.println(rectangle.getArea());
	}
}

결과는 50이 아닌 25를 반환한다. 잘못된 상속이기 때문. 이를 해결하기 위해서는 Shape 이라는 더 상위 개념인 사각형 객체를 구현하고 Square 과 Rectangle이 상속 받으면 된다. 

public class Shape{
	protected int width;
	protected int height;
    
	public int getWidth(){return width;}
	public int getHeight(){return height;}
	public void setWidth(int width){this.width = width;}
	public void setHeight(int height){this.height = height;}
	public int getArea(){return width * height;}
}

리스코프 치환 원칙을 어기면 개방 폐쇄 원칙을 어길 가능성이 높아진다. 

 

ISP (Interface Segregation Principle)

인터페이스 분리 원칙. 클래스에 의해 구현되는 더 작고 더 구체적인 일련의 인터페이스를 작성해야 한다. 인터페이스 단일화 라고 생각하면 쉽다. 보통 클라이언트가 사용하는 기능을 중심으로 인터페이스를 분리함으로써 클라이언트로부터 발생하는 변경 여파가 다른 클라이언트에 미치는 영향을 최소화할 수 있다.

public interface Printer {
	copyDocument();
	printDocument(Document document);
	stapleDocument(Document document, int tray);
}

class SimplePrinter implements Printer {
	public copyDocument() {}
    
	public printDocument(Document document){
		console.log("");
	}
    
	public stapleDocument(Document, int tray) {}
}

 

public interface Printer {
	printDocument(Document document);
}

public interface Stapler {
	stapleDocument(Document document, int tray);
}

public interface Copier {
	copyDocument();
}

class SimplePrinter implements Printer {
	public printDocument(Document document){}
}

class SuperPrinter implements Printer, Stapler, Copier {
	public copyDocument(){}
	public printDocument(Document document){}
	public stapleDocument(Document document, int tray){}
}

 

DIP (Dependency Inversion Principle)

의존 역전 원칙. 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대 의존해서는 안된다. 더 구체적으로, import, include 와 같은 구문은 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다.

  • 고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈
  • 저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현

고수준 모듈이 저수준 모듈에 의존하게 되면 저수준 모듈의 기능이 변경할 때마다 고수준 모듈을 수정해야 하는 상황이 발생하게 된다. 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않는 것인데 이를 위한 원칙이 의존 역전 원칙이다.

예제1)

class CarWindow {
	public void open() {...}
	public void close() {...}
}

class WindowSwitch {
	private isOn = false;
    
	private WindowSwitch(CarWindow window) {}
    
	public void onPress() {
		if(this.isOn) {
			this.window.close();
			this.isOn = false;
		} else {
			this.window.open();
			this.isOn = true;
		}
	}
}

↑ DIP가 지켜지지 않음

public interface IWindow {
	public void open();
	public void close();
}

class CarWindow implements IWindow {
	public void open(){...}
	public void close(){...}
}

class WindowSwitch {
	private isOn = false;
    
	private WindowSwitch(IWindow window){}
    
	public void onPress(){
		if(this.isOn) {
			this.window.close();
			this.isOn = false;
		} else {
			this.window.open();
			this.isOn = true;
		}
	}
}

의존성 역전 : 고수준에서 저수준을 의존하는 것을 인터페이스를 통해 역전시킴. 제어의 흐름과는 반대 방향으로 역전시키게 됨.

예제2)

// A사의 알람 서비스
public class A {
	public String beep(){
    	return "beep!";
    }
}

 

// 서비스 코드
public class AlarmService {
	private A alarm;
	public String beep() {
		return alarm.beep();
	}
}

문제

  1. A → AlarmService 테스트 순서화. A가 완벽히 구현되어야만 테스트 가능
  2. 확장 및 변경이 어려움. B사가  추가 되면
public class AlarmService{
	private A alarmA;
	private B alarmB;
    
	public String beep(String company){
		if(company.equals("A")){
			return alarmA.beep();
		} else {
			return alarmB.beep();
		}
	}
}

 

public interface Alarm{
	String beep();
}

public class A implements Alarm {
	@Override
	public String beep(){
		return "beep!";
	}
}

public class AlarmService {
	private Alarm alarm;
    
	public AlarmService(Alarm alarm){
		this.alarm = alarm;
	}
    
	public String beep(){
		return alarm.beep();
	}
}

의존 역전 원칙은 리스코프 치환 원칙과 개방 폐쇄 원칙의 기반이 되는 원칙이다. 인터페이스는 고수준 입장에서 만들어지는데 이것은 고수준 모듈이 저수준 모듈에 의존했던 상황이 역전되어 저수준 모듈이 고수준 모듈에 의존하게 되었다는 것을 의미한다. 그러나 소스 코드 상에서의 의존은 역전되었지만 런타임에서의 의존은 여전히 고수준 모듈 객체에서 저수준 모듈 객체로 향한다.

 

정리

SRP와 ISP는 객체가 커지는 것을 막아준다. 객체 하나가 하나의 기능을 갖게하고 각 객체마다 인터페이스를 구현함으로써 한 기능의 변경이 다른 곳까지 미치는 영향을 최소화하고, 기능 추가 및 변경에 용이하도록 만들어준다.

LSP와 DIP는 OCP를 서포트한다. 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장에는 용이하되 기존 코드 변화에는 보수적이도록 만들어준다.

 

더보기

 

코드리뷰

  • 전체적으로 강사님이 짜주신 BaseBallTeam 구조는 잘 구조화 되어있는 듯 했다. Data접근 / Dao / Dto ...
  • Write- interface를 만들어 관리하면 더 객체지향적 설계가 될 것 같다!
chatting 프로젝트 파일 목록

 

 

[예시] https://bbaktaeho-95.tistory.com/98

 

[Programming] SOLID 원칙 (객체지향 5대 원칙, SRP, OCP, LSP, ISP, DIP)

들어가며 좋은 소프트웨어는 깔끔한 코드로부터 시작한다. - 로버트 C. 마틴 건물을 지을 때 좋은 벽돌을 사용하지 않으면 건물의 구조가 좋고 나쁨은 큰 의미가 없다고 합니다. 반대로 좋은 벽

bbaktaeho-95.tistory.com

[리스코프] https://blog.itcode.dev/posts/2021/08/15/liskov-subsitution-principle

 

[OOP] 객체지향 5원칙(SOLID) - 리스코프 치환 원칙 (Liskov Subsitution Principle) - 𝝅번째 알파카의 개발

리스코프 치환 원칙은 부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다. 객체지향 언어에선 객체의 상

blog.itcode.dev

[개념] https://koseungbin.gitbook.io/wiki/books/undefined/part-2.-di/solid/dependency-inversion-principle

 

의존 역전 원칙(Dependency Inversion Principle) - Developer Log

바이트 데이터를 암호화한다는 것은 프로그램의 의미 있는 단일 기능으로서 고수준 모듈에 해당한다. 고수준 모듈은 데이터 읽기, 암호화, 데이터 쓰기라는 하위 기능으로 구성되는데, 저수준

koseungbin.gitbook.io

 

'Study > Lotte Study' 카테고리의 다른 글

Spring web MVC  (0) 2022.07.04