장쫄깃 기술블로그

[Java] 객체 지향 설계 5원칙 - SOLID 본문

Programming Language/Java

[Java] 객체 지향 설계 5원칙 - SOLID

장쫄깃 2022. 4. 20. 16:26
728x90

 


객체 지향 설계 5원칙 - SOLID


객체 지향 설계의 정수라고 할 수 있는 5원칙이 집대성되었는데, SOLID 원칙이다. SOLID는 각 5가지 원칙의 앞 머리 알파벳을 따서 부르는 이름이다.

 

  • SRP(Single Responsibility Principle) : 단일 책임 원칙
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존 역전 원칙

 

 

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


어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- 로버트 C.마틴

 

단일 책임 원칙이란 모든 클래스는 각각 하나의 책임만을 가져야 한다는 뜻이다. 클래스의 역할과 책임을 너무 많이 주어서는 안 된다.

 

즉, 클래스를 설계할 때 책임의 경계를 정하고, 추상화를 통해 경계 안에서 필요한 속성과 메소드를 선택하여 설계해야 한다.

 

SRP를 지키지 못하는 경우 한 클래스가 너무 많은 역할을 가질 수 있다.

public class Bike {
	private final static int TWO_WHEEL = 2;
	private final static int FOUR_WHEEL = 4;
	
	public void run(int wheel) {
		if(wheel == TWO_WHEEL) {
			// 두 바퀴로 가는 자전거
		}else if(wheel == FOUR_WHEEL) {
			// 네 바퀴로 가는 자전거
		}else {
			// 바퀴가 없는 자전거
		}
	}
}

위 자전거 클래스 코드를 보면 run() 메소드에서 두 발 자전거, 네 발 자전거를 모두 구현하고 있다. 한 클래스가 여러 역할을 가져 단일 책임 원칙을 위반하고 있다.

abstract class Bike {
	public abstract void run(int wheel);
}

public class TwoWheelBike extends Bike {
	@Override
	public void run(int wheel) {
		// 두 바퀴로 가는 자전거
	}
}

public class TwoWheelBike extends Bike {
	@Override
	public void run(int wheel) {
		// 네 바퀴로 가는 자전거
	}
}

그래서 이처럼 Bike라는 추상 클래스를 두고 TwoWheelBike, FourWheelBike 클래스에 각자 자신의 특징에 맞게 run() 메소드를 재정의해서 사용하여 한 클래스가 하나의 책임만 가지도록 했다.

 

 

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


소프트웨어 엔티티는 확장에 대해서는 열려있어야 하지만 변경에 대해서는 닫혀있어야 한다.
- 로버트 C.마틴

 

즉, 개방 폐쇄 원칙이란 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀있어야 한다는 것이다. 여기서 엔티티는 클래스, 함수, 모듈 등과 같은 것을 뜻한다.

public class 마티즈 {
	void 창문조작() {
		// 수동 창문조작
	}
    
	void 기어조작() {
		// 수동 기어조작
	}
}

public class 쏘나타 {
	void 창문조작() {
		// 자동 창문조작
	}
    
	void 기어조작() {
		// 자동 기어조작
	}
}

위의 그림을 보면 운전자가 기어가 수동 or 자동이냐에 따라 행동이 달라지는 것을 볼 수 있다. 이렇게 어떤 변화가 있을 때 바로 사용자에게 영향이 오는 설계는 개방 폐쇄 원칙에 위배된다.

public interface 자동차 {
	void 창문조작();
	void 기어조작();
}

public class 마티즈 implements 자동차 {
	void 창문조작() {
		// 수동 창문조작
	}
    
	void 기어조작() {
		// 수동 기어조작
	}
}

public class 쏘나타 implements 자동차 {
	void 창문조작() {
		// 자동 창문조작
	}
    
	void 기어조작() {
		// 자동 기어조작
	}
}

이렇게 상위 클래스 또는 인터페이스를 둠으로써 다양한 자동차가 생긴다고 해도 사용자는 자동차라고 하는 객체만 바라보면 되기 때문에 변화에 영향을 크게 받지 않는다. 다양한 자동차가 생긴다고 하는 것은 자동차 입장에서는 자신의 확장에는 개방되어있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄되어 있는 것이다.

 

 

JDBC가 개방 폐쇄 원칙의 가장 좋은 예다. 데이터베이스가 MySQL에서 Oracle로 바뀌더라도 Connection을 설정하는 부분만 바꿔주면 되기 때문이다.

 

개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.
따라서 객체지향 프로그래밍에서 개방 폐쇄 원칙은 반드시 지켜야 할 원칙이다.

 

 

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


서브 타입은 언제나 자신의 가반 타입으로 교체할 수 있어야 한다.
- 로버트 C.마틴

 

리스코프 치환 원칙이란 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다는 것이다.

 

  • 하위 클래스가 상위 클래스의 한 종류 - 하위 클래스 is a kind of 상위 클래스
  • 구현 클래스는 인터페이스 가능 - 구현 클래스 is able to 인터페이스

이 두 문장을 잘 지키고 있다면, 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다.

 

리스코프 치환 원칙은 인터페이스와 클래스의 관계, 상위 클래스와 하위 클래스 관계를 얼마나 잘 논리적으로 설계했느냐가 관건이다.

 

리스코프 치환 원칙 위배(계층도/조직도)

위에서 말했듯 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해도 문제가 없어야 한다. 사진에서 보면 아들은 곧 아버지고 할아버지일 수 있지만 딸은 아버지나 할아버지가 될 수 없다. 사촌 형은 삼촌이나 할아버지가 될 수 있지만 사촌누나는 삼촌이나 할아버지가 될 수 없다. 이는 상위형 객체 참조 변수에 대입이 불가능한 상황이기 때문에 리스코프 치환 원칙에 위배된다.

리스코프 치환 원칙 만족(분류도)

사진에서 보면 고래나 박쥐 모두 포유류이며 동물이다. 마찬가지로 참새나 펭귄은 조류이며 동물이다. 이렇게 상위형 객체 참조 변수에 대입해도 문제가 없을 시 리스코프 치환 원칙을 만족했다고 볼 수 있다.

 

다시 한번 강조하자면

하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.

 

 

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


클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.
- 로버트 C.마틴

 

인터페이스 분리 원칙은 사용자의 상황과 관련이 있는 메소드만 제공하는 제공하라는 뜻이다. 인터페이스 분리 원칙은 단일 책임 원칙과 같은 원인에 다른 해결책을 제시하는 것이다. 너무 많은 책임을 주어 상황에 관련되지 않은 메소드까지 구현했다면, 단일 책임 원칙은 클래스를 여러 개로 나눈다. 하지만 인터페이스 분리 원칙은 해당 클래스는 수정하지 않고, 인터페이스 최소주의 원칙에 따라 각 상황에 맞는 기능만 인터페이스로 분리해서 사용자에게 제공하도록 한다.

 

public interface 요리 {
	void cook();
}

public interface 운동 {
	void exercise();
}

// 요리와 운동을 하는 철수 구현
public class 철수 implements 요리, 운동 {
	@Override
	public void cook() {
		// ...
	}
    
	@Override
	public void exercise() {
		// ...
	}
}

// 요리를 하는 영희 구현
public class 영희 implements 요리 {
	@Override
	public void cook() {
		// ...
	}
}

public class Main {
	public static void main(String[] args) {
		// 운동하는 철수가 필요할 때
		운동 cs = new 철수();
		cs.exercise();
		
		// 요리하는 영희가 필요할 때
		요리 yh = new 영희();
		yh.cook();
	}
}

철수는 운동과 요리를 다 하지만 운동하는 철수만 필요한 경우 상황에 맞는 메소드만 제한하여 사용 가능하게 하는 것이 인터페이스 분할 원칙이다.

 

인터페이스 분할 원칙은 상위 클래스는 풍성할수록, 인터페이스는 작을수록 좋다.

결론적으로는 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다.

하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다.

 

 

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


차원 모듈은 저차원 모듈에 의존하면 안 된다.
이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.
추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.
자주 변경되는 구체(Concrete) 클래스에 의존하지 마라.
- 로버트 C.마틴

 

의존 역전 원칙은 자식 또는 구채화된 클래스에 의존하면 안 된다는 뜻이다. 즉, '자신보다 변하기 쉬운 것에 의존하지 마라'입니다.

 

추상 클래스 또는 상위 클래스는 구체적인 구현 클래스 또는 하위 클래스에 의존적이면 안된다. 왜냐하면 구체적인 클래스는 코딩에 있어서 가장 전면에 노출되고 사용되어 변화에 민감하기 때문이다. 만약 의존 역전 원칙을 지키지 않으면 구체화된 클래스가 수정될 때마다 상위 클래스나 추상 클래스가 변화해야 한다. 만약 그 상위에 연관되어 있는 클래스가 있다면 거기까지 수정되어야 한다. 따라서 하위 클래스나 구체화된 클래스에 의존하면 안 된다.

 

자동차가 타이에어 의존하면 어떻게 될까? 자동차에서 타이어는 매우 자주 바뀌는 부품 중 하나다. 이렇게 자주 바뀌는 것에 의존하면 자동차는 영향을 받게 된다. 즉, 자동차 자신보다 더 자주 면하는 스노우타이어에 의존하기에 좋지 않음을 알 수 있다.

자동차가 구체적인 타이어(스노우타이어 같은)가 아니라 추상화된 타이어 인터페이스에만 의존하게 함으로써 타이어가 변경되어도 자동차가 영향을 받지 않게 된다.

처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존 역전 원칙이다.

 

 

정리하며


객체 지향 프로그래밍을 하며 SOLID 원칙을 지키는 가장 큰 이유는 자기 자신 클래스 내부의 응집도는 높이고, 외부 클래스들 간의 결합도는 낮추는 High Cohesion - Loose Coupling 원칙을 지키기 위함이다. 좋은 소프트웨어는 응집도가 높고 결합도가 낮다. 결국 모듈 또는 클래스 당 하나의 책임을 주어 독립된 객체를 만들기 위함이다.

 

결국 개발자란 소프트웨어를 개발하고 유지보수하는 일을 한다. 개발 시 재사용률을 높이고 유지보수 시 수정을 편하고 최소화하기 위해서 SOLID 원칙을 지킬 필요가 있다.

728x90