반응형

 

객체지향 설계에서 지켜야 할 5개의 원칙( SRP, OCP, LSP, DIP, ISP )을 SOLID 원칙이라고 한다.

 

SOLID 원칙은 시스템에 예상하지 못한 변경사항이 발생하였을 때 유연하게 대처하고 확장성이 있는 시스템 구조를 설계하는데 도움이 된다.

 

또한, 좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다.

 

 

 

1. 단일 책임 원칙: SRP( Single Responsibility Principle )

 

SRP는 객체는 단 하나의 책임만 가져야 한다는 원칙을 의미한다.

여기서 책임의 기본 단위는 객체를 의미하며 하나의 객체가 하나의 책임을 가져야 한다는 의미이다.

 

객체지향적으로 설계할 때는 응집도를 높게, 결합도는 낮게 설계하는 것이 좋다.

 

하나의 객체에 책임이 많아질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아지며, 객체마다 책임을 제대로 나누지 않는다면 시스템이 복잡해지기 때문이다.

 

따라서 여러 객체들이 하나의 책임만 갖도록 분배한다면, 시스템에 변화가 생기더라도 그 영향을 최소화 할 수 있기 때문에 SRP 원칙을 따르는 것이 좋다.

 

AOP(Aspect Oriented Programming) 또한 SRP의 예제가 될 수 있다. 여러개의 클래스가 로깅이나 보안, 트랜잭션과 같은 부분은 공유하고 있을 수 있다. 이런 부분을 모듈화를 통해 각각 필요한 곳에 위빙해주는 방식을 위해 도입된 AOP또한 로깅, 보안, 트랜잭션과 같은 부분을 하나의 모듈에 단일책임으로 부여하여 이를 사용하게 할 수 있도록 함으로써 SRP를 지키는 방법이다.

 

// SRP에 위배
public class Calculator {
	public void add(){}
	public void sub(){}
	public void mult(){}
	public void div(){}
	public void alarm(){}
}

 

// SRP를 만족
public class Calculator {
  	public void add(){}
   	public void sub(){}
   	public void mult(){}
   	public void div(){}
   	public void alarm(){}
}

public class Alarm {
	public void setTime(){}
	public void ring(){}
}

 

 

 

2. 개방-폐쇄 원칙: OCP ( Open-Closed Principle )

 

OCP는 기존의 코드를 변경하지 않으면서( closed ), 기능을 추가할 수 있도록( open ) 설계가 되어야 한다는 원칙이다.

 

OCP에서 중요한 것은 요구사항이 변경되었을 때 코드에서 변경되어야 하는 부분과 변경되지 않아야하는 부분을 명확하게 구분하여, 변경되어야 하는 부분을 유연하게 작성하는 것을 의미한다.

 

즉, 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 의미를 갖는다.

 

이를 만족하는 설계가 되려면, 캡슐화를 통해 여러 객체에서 사용하는 같은 기능을 인터페이스에 정의하는 방법이 있다.

 

public interface Animal {
  	public void crying(){}
}

public class Cat implements Animal {
  	public void crying(){}
}

public class Dog implements Animal {
  	public void crying(){}
}

 

캡슐화를 통해 클래스가 추가되었을 때 cyring() 함수를 호출하는 부분은 건드릴 필요가 없이 쉽게 확장할 수 있다

//인터페이스를 구현한 클래스들은 crying() 함수를 재정의한다
public class Client {
	public static void main(String args[]){
		Animal cat = new Cat();
		Animal dog = new Dog();

		cat.crying();
		dog.crying();
	}
}

 

 

 

3. 리스코프 치환 원칙: LSP ( Liskov Substitution Principle )

 

LSP는 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 원칙이다.

 

자식 클래스는 항상 부모 클래스의 역할을 대체할 수 있어야 한다는 것을 의미하며, 부모 클래스와 자식 클래스의 행위가 일관됨을 의미한다.

 

자식 클래스가 부모 클래스를 대체하기 위해서는 가급적 부모의 기능에 대한 오버라이드 피해야 한다.

즉, 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족하게 된다.

 

//부모클래스
public class Bag {
    private double price;
    
    public double getPrice() {
    	return price;
    }
    
    public void setPrice(double price) {
    	this.price = price;
    }
}

//자식클래스
public class DiscountedBag extends Bag{
    private double discountRate;
    
    public void setDiscountRate(double discountRate) {
    	this.discountRate = discountRate;
    }
    
    public void applyDiscount(int price) {
    	super.setPrice(price- (int)(discountRate * price));
    }
}

 

 

 

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

ISP는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 설계 원칙이다.

즉, 하나의 거대한 인터페이스 보다는 여러 개의 구체적인 인터페이스가 낫다는 것을 의미한다.

 

SRP가 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다.

 

//ISP 위배
public interface Phone {
	public void call(){}
	public void sms(){}
	public void alarm(){}
}

public class Samsung implements Phone {
	public void call(){}
	public void sms(){}
	public void alarm(){}
}

public class Apple implements Phone {
	public void call(){}
	public void sms(){}
	public void alarm(){}
}

 

public interface Call {
	public void call(){}
}

public interface Sms {
	public void sms(){}
}

public interface Alarm {
	public void alarm(){}
}


public class Samsung implements Call, Sms, Alarm {
	public void call(){}
	public void sms(){}
	public void alarm(){}
}

public class Apple implements Call, Sms, Alarm {
	public void call(){}
	public void sms(){}
	public void alarm(){}
}

 

위와 같은 설계를 통해 다양 기능을 인터페이스화함으로써 클라이언트에서 인터페이스를 사용할 때 타 인터페이스의 영향을 받지 않고 본인이 구현하고자 하는 기능만을 선택하여 사용할 수 있게 된다.

 

 

 

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

DIP는 객체들이 정보를 주고 받으며 의존 관계가 형성될 때, 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙이다.

 

이때, 원칙이란 추상성이 낮은 클래스보다 추상성이 높은 클래스와 의존 관계를 맺어야 한다는 것을 의미한다.

 

DIP를 만족하려면 어떤 클래스가 도움을 받을 때 혹은 의존할 때 구체적인 클래스는 변화할 확률이 높기 때문에 이를 추상화한 인터페이스나 추상 클래스와 의존관계를 맺도록 설계해야 한다. 

 

public class Kid {
	private Toy toy;
    
	public void setToy(Toy toy) {
		this.toy = toy;
	}
    
	public void play() {
		System.out.println(toy.toString());
	}
}
    
public class Lego extends Toy{
	@Override
	public String toString() {
		return "Lego";
	}
}

public abstract class Toy{
	public abstract String toString();
}
    
public class Main {
	public static void main(String[] args) {
		Kid kid = new Kid();
		kid.setToy(new Lego());
		kid.play();
	}
}

 

위의 코드는 abstract class 혹은 interface 를 통해 관리함으로써 변경사항에 대해서 유연하게 대처할 수 있고 변화하는 부분을 추상화하여 변화되지 않는 형태로 만든 추상클래스를 의존하기 때문에 DIP원칙과 OCP 둘다 만족하는 형태를 갖는다.

 

 

 

 

 

 

 

# 참고사이트

https://victorydntmd.tistory.com/291?category=719467

https://velog.io/@kyle/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-SOLID-%EC%9B%90%EC%B9%99-%EC%9D%B4%EB%9E%80

 

 

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기