상세 컨텐츠

본문 제목

[객체지향] 객체지향 설계의 5원칙

CS

by 노베이스 컴공학도 2023. 1. 12. 21:34

본문

오늘의 썸네일 주인공은 주황 튤립이다. 꽃말은 '수줍음', '부끄러움', '온정', '매혹적인 사랑' 이라고 한다. 나는 수줍음과 부끄러움이 많은 편인듯하다. 이런 모습도 나름 매력이라고 생각하지만, 그래도 하고싶은 말이 있을 때는 용기를 낼 줄 아는사람이 되고싶다. 올 해는 한 번 노력해보는 걸로~

 

객체지향의 설계 5원칙, 이것도 기본인 것 같아서 이번 기회에 한 번 글로 써두고 가려고 한다. 흔히 SOLID라고 많이 부르는데 이게 뭔지? 하나부터 둘까지 알아보도록 하자.

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

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

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

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

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

 

먼저 SRP, 단일 책임 원칙이다.

설명은 '한 클래스는 하나의 책임만 가져야 한다' 라고 정리가 되어있지만 이걸로 이해하기는 좀 어려운 것 같다. 해당 클래스를 변경해야하는 이유가 한 가지여야만한다. 라는 설명이 조금 더 와닿는 것 같다. 코드로 구현을 해볼까 했는데.. 정해진 정답이 없다. 하나의 책임이라는게 꼭 하나의 메서드를 이야기 하는 것도 아니고, 그냥 클래스에 여러 변수와 메서드들이 존재할 수는 있지만, 결국 하나의 기능을 수행하기 위해 존재하는 것들이며 이 기능을 변경하는 경우에만 수정된다 이런 이야기인데... 아무튼 단순히 면접용 답변이라고 생각한다면 '한 클래스는 하나의 책임만 가져야 하고, 그 이유는 하나의 책임을 가져야만 결합도가 낮아지고 문제점이 발생했을 때 어떤 부분을 수정해야 하는지 찾기 쉬워 유지보수에 유리하기 때문이다' 정도가 될 것 같다. 

 

OCP, 개방 폐쇄 원칙이다.

해석하자면 확장에는 열려 있어야 하며, 수정에는 닫혀 있어야 한다 라는 뜻이다. 추상화와 깊은 관려이 있다. 추상클래스나 인터페이스 모두 상속 및 구현을 통해 기존의 기능을 확장해서 사용이 가능하다. 그렇지만 상속받은 클래스를 수정한다고 해서, 부모 클래스 및 인터페이스를 직접적으로 수정하는 것은 아니다.

public class SOLID {

	public static void main(String[] args) {	
		Dog seechoo = new Dog();
		seechoo.bark("시츄");
	}
	public static class Dog{
			public void bark(String dog) {
				if(dog=="시츄") {
					System.out.println("시츄르르르왈왈");
				}
				else if(dog=="치와와") {
					System.out.println("치왈와왕와왕");
				}
				else if(dog=="시바") {
					System.out.println("시바시바시바멍");
				}
			}
	}
}

만약 Dog 클래스 안에 bark라는 메서드에 다른 개들의 짖는 소리를 추가하고 싶으면 계속해서 조건문으로 추가를 해주어야 하는데 이럴 경우 class를 직접 수정하기 때문에 수정에는 닫혀 있다는 원칙을 위반하게 된다. 따라서

public class SOLID {

	public static void main(String[] args) {
		SeeChu seechu = new SeeChu();
		seechu.bark();
	}

	public static class Dog {
		public void bark() {

		}
	}

	public static class SeeChu extends Dog {
		public void bark() {
			System.out.println("시츄르르르왈왈");
		}
	}
	
	public static class Seebar extends Dog {
		public void bark() {
			System.out.println("시바시바멍멍");
		}
	}
}

이렇게 짖는다는 공통된 메서드를 추상화시켜서 Dog클래스에 두고 이를 상속해서 시츄, 시바 등 자식 클래스들을 만든다면 확장에는 열려있지만(개가 짖는 소리를 원하는대로) 수정에는 닫혀있다.(개가 짖는 소리를 원하는대로 작성해도 원래 Dog 메서드에 있는 bark()코드가 변경되는건 아니다)

 

LSP  리스코프 치환 원칙이다

리스코프 치환 원칙은 하위 클래스는 상위 클래스를 대체할 수 있어야 한다는 뜻이다. 또한 하위 클래스에서는 인터페이스 규약을 지켜야 한다. 단순히 컴파일 에러가 나지 않는것 뿐만 아니라 기능의 명세까지 잘 지켜야 한다. 예를 들어 자동차에 쓰일 brake 라는 인터페이스를 만들었다. 이 인터페이스는 당연히 속도에 제동을 거는 장치이기 때문에 구현시에 현재 속도가 줄어들어야 하는데 속도가 늘어나는 식으로 구현을 하게 되면 컴파일 에러는 나지 않을지라도 LSP 원칙을 지키지 않은 것이다. 다시 한 마디로 정리하자면 하위 타입이 상위 타입을 대체할 수 있어야 한다는 이야기다. 위의 코드를 예로 들자면 Dog dog = new Dog() 으로 dog 인스턴스로 하던 일이 있는데, 이걸 SeeChu dog = new Seechu()로 Dog의 하위 클래스인 SeeChu로 변경한다고 해도 동작에 전혀 이상이 없어야 한다는 뜻이다.

 

ISP 인터페이스 분리 원칙이다.

객체는 자신이 호출하지 않는 메서드(사용하지 않는 메서드)에 의존하지 않아야 한다는 규칙이다. 바로 코드로 들어가자

	public abstract static class Animal {
		abstract void fly();
		abstract void run();
	}

	public static class Bird extends Animal {
		@Override
		void fly() {
			System.out.println("새가 난다요");
		}
		@Override
		void run() {
			System.out.println("새가 뛴다요");
		}
	}
	
	public static class Dog extends Animal {
		@Override
		void fly() {
			System.out.println("개는 못난다요");
		}

		@Override
		void run() {
			System.out.println("개가 뛴다요");
		}

	}

동물이라는 추상클래스를 상속받는 개,새가 있다(욕 아님). 새 같은 경우 날고 뛰고 다한다. 근데 개는 못나는데 동물이기 때문에 어쩔수없이 사용하지도 않는 fly라는 메서드를 오버라이딩 해야한다. 이럴 경우 ISP 원칙을 위배하게 된다. 따라서 이를 해결하기 위해서는 상위 클래스에서 범용적으로 선언할 것이 아니라, 특정 행동을 필요로 하는 객체만 Interface로 해당 기능을 상속받도록 해야한다. 즉 범용성 있는 인터페이스 하나보다 구체적인 인터페이스 여러개가 낫다. 이걸 수정해서 코드로 나타내면

	public abstract static class Animal {
		abstract void run();
	}

	interface Flyable{
		void fly();
	}
	
	public static class Bird extends Animal implements Flyable {
		@Override
		public void fly() {
			System.out.println("새가 난다요");
		}
		@Override
		void run() {
			System.out.println("새가 뛴다요");
		}
	}
	
	public static class Dog extends Animal {
		@Override
		void run() {
			System.out.println("개가 뛴다요");
		}

	}

이렇게 동물이라는 클래스에는 Run()만 두고, 새같이 나는 녀석들만을 위한 Flyable이라는 인터페이스를 만들어주고 Bird 클래스가 이 인터페이스를 구현한다. 그럼 애니멀의 '새' 자식 클래스는 run()은 부모 클래스 애니멀로부터, fly()는 인터페이스 Flyable로 부터 구현하면된다. 그리고 애니멀의 '개' 자식 클래스는 자기가 필요한 run()메소드만 애니멀 부모 클래스로부터 받아올 수 있게 된다.

 

마지막 DIP, 의존성 역전 원칙이다.

변화하기 쉬운 것에 의존하지 말고 잘 변화하지 않는 것에 의존해라 라는 뜻이라는데, 구체적으로 구현된 클래스보다는 추상 클래스 및 인터페이스에 의존해서 관계를 형성하라는 뜻이다. 상위 모듈에 대한 종속성을 줄이기 위해서라고 한다. 이 개념은 다른 개념들의 하위호환 느낌? 중복되는 부분도 많고 아무튼 의미만 기억해두자. 잘 변하는 상위 모듈에 의존하면 하위 모듈에서도 그 변화에 맞게 코드를 수정해야 할 일이 많이 생긴다. 그러나 전적으로 하위 모듈에 구현이 달려있는 추상클래스나 인터페이스에 의존한다면 상위 모듈의 변화가 하위 모듈에 크게 영향을 끼치지 않는다.

 

그럼 왜 이 5가지 원칙을 지키면서 객체지향 설계를 해야 하나??

 이 5가지 원칙을 보아서 알겠지만 대부분 추상화, 상속과 관련된 이야기이다. 하나의 책임을 지는 적절한 클래스를 만들고, 적절한 추상화로 인터페이스와 추상 클래스를 만들고 이를 이용해 하위클래스를 만들고 이런 과정들을 거쳐서 얻고자 하는것은 결국 객체지향의 장점들이다. 책임소재를 확실히 하여 유지보수에 좋고, 추상화를 통해 재사용성 및 확장에 유리하다. 결국 4가지 특성과 5가지원칙이 이야기하고자 하는것은 바로 객체지향의 이런 장점들이고, 이런 장점을 활용하는 규칙에 대한 내용이다.

 

객체지향, 나름 열심히 보고 썼다고는 하지만 아직 완벽하게 이해가 된 것 같지는 않다. 아마 개발할 때 이런 것들을 생각하면서 한 적이 없기 때문에 실제 예가 잘 안 떠오르고 적용도 잘 안돼서 그런 것 같다. 다음에 어떤 프로젝트를 하게 된다면 이 개념들을 다시 보러 와야겠다. 그 때 다시 보고 부족한 내용이 있으면 보충하러 와야겠다.

 

내일은 뭘 쓸까.. 음 자바와 관련된 백엔드 쪽이 부족한 것 같으니 내일은 스프링을 기초부터 알아봐야겠다.

관련글 더보기