2023. 2. 4. 03:37ㆍOOP
SOLID란?
컴퓨터 프로그래밍 중 객체지향 패러다임에서 객체지향을 설계할때 가장 기본적인 다섯가지 기본 원칙입니다.
Single Responsibility Principle( 단일 책임 원칙 ), Open-Closed Principle ( 개방폐쇄의 원칙 ),
Liskov Substituation Principle ( 리스코프 치환 원칙 ), Interface Segregation Principle ( 인터페이스 분리의 원칙 ),
Dependency Inversion Principle ( 의존 역전의 원칙 ) 의 앞 글자를 따서 SOLID라고 이야기합니다.
Single Responsibility Principle, 단일 책임 원칙
단일 책임 원칙은 클래스는 하나의 책임만 가져야 한다는 원칙입니다.
예를 들어 자동차라는 제품을 만든다고 가정할때, 핸들, 페달, 시트 등 각각의 클래스를 만들고 특정 부분에서 오류가 발생하면 해당 클래스만 수정하면 되기 때문에 유지보수에 큰 이점을 가져올 수 있습니다.
실제로 저희는 시트에 대해서 고장이 나면 시트만을 교체하지, 핸들, 페달을 같이 교체하지 않습니다.
package week1.single;
public class SingleResponsibilityPrinciple {
public static void main(String[] args) {
Car car = new Car(new Handle(), new Pedal(), new Sheet());
car.handle.turn();
car.pedal.run();
car.sheet.move();
}
}
class Car {
public Handle handle;
public Pedal pedal;
public Sheet sheet;
public Car(Handle handle, Pedal pedal, Sheet sheet) {
this.handle = handle;
this.pedal = pedal;
this.sheet = sheet;
}
public void drive() {
System.out.println("drive!!");
}
}
class Handle {
public void turn() {
System.out.println("turn");
/***
* code ..
*/
}
}
class Pedal {
public void run() {
System.out.println("run");
/***
* code ..
*/
}
}
class Sheet {
public void move() {
throw new RuntimeException("move exception");
/***
* code ..
*/
}
}
예제 코드입니다.
위처럼 Sheet의 move 메소드에서 예외가 발생하면 Sheet 클래스에 대해서만 수정을 거치면 정상적으로 동작하게 됩니다.
Open-Closed Principle, 개방 폐쇄원칙
개방 폐쇄의 원칙은 확장에 열려있어야 하지만 변경에는 닫혀있어야 한다는 원칙입니다.
예를 들어 동물이라는 클래스에서 울기라는 메소드가 있고, 동물을 구분하는 것이 파라미터의 값을 토대로 구분하게 된다면
동물의 종류가 늘어나면 늘어날수록 해당 메소드는 값을 구분하는 조건문을 추가해야 할것입니다.
이는 상속과 오버라이딩을 통하여 해결할 수 있습니다.
기존 코드
class Animal {
String type;
public Animal(String type) {
this.type = type;
}
public void 울기() {
if (type.equals("강아지")) {
System.out.println("wal wal");
} else if (type.equals("고양이")) {
System.out.println("meow meow");
} else if (type.equals("소")) {
System.out.println("moo");
}
}
}
모두 Animal이라는 타입을 갖고 있지만 매번 Type에 따라서 울기 메소드를 수정해야 합니다.
이는 추가적인 작업이 필요할 뿐만 아니라 실수로 분기를 추가하지 않을 시 예외가 발생할 수 있습니다.
수정 코드
class Animal {
public void 울기() {
}
}
class Dog extends Animal {
@Override
public void 울기() {
System.out.println("wal wal");
}
}
class Cat extends Animal {
@Override
public void 울기() {
System.out.println("meow");
}
}
public class OpenClosedPrinciple {
// 개방 폐쇠의 원칙
public static void main(String[] args) {
Animal 동물 = new Animal();
동물.울기();
동물 = new Dog();
동물.울기(); //wal wal
동물 = new Cat();
동물.울기(); // meow meow
}
}
위와 같이 수정 후 단순히 Animal의 메소드인 울기를 오버라이딩 해주면 됩니다.
이를 통하여 분기를 통한 동작이 아닌 클래스에 따라서 메소드의 행위가 정해지게 됩니다.
※ 오버라이딩, 오버로딩
오버 라이딩은 상속을 받은 클래스에서 상위 클래스의 메소드를 재정의 하는 것입니다. ( 혹은 인터페이스 구현 )
오버 로딩은 메소드의 들어오는 매개변수의 타입 혹은 그 수를 기준으로 같은 이름의 메소드를 여러개 만들 수 있는 기능입니다.
이를 통하여 재사용성과 다향성의 이점을 가질 수 있게됩니다.
Liskov Substitution Principle, 리스코프 치환 원칙
리스코프 치환의 원칙은 상위 타입의 객체를 하위타입의 객체로 치환해도 정상적으로 동작해야 한다는 원칙입니다.
상속을 받음에도 해당 원칙이 지켜지지 않는다면 상속을 하지 말아야 한다는 원칙입니다.
만약 우리가 SportCar라는 클래스를 만든다고 해봅시다 이는 Car클래스를 상속받습니다.
SportCar.class
class SportCar extends Car {
public SportCar(Handle handle, Pedal pedal, Sheet sheet) {
super(handle, pedal, sheet);
}
@Override
public void drive() {
super.drive();
}
}
여기서 더 다이나믹한 운전을 위해 스포츠 핸들 또한 만들었습니다.
스포츠 핸들은 핸들을 상속받습니다.
SportHandle.class
class SportHandle extends Handle{
public void turn() {
System.out.println("turn");
/***
* code ..
*/
}
}
하지만 모종의 이유로
Car의 drive 메소드는 일반 핸들만을 사용해야 실행이 가능하다고 합니다.
class Car {
public Handle handle;
public Pedal pedal;
public Sheet sheet;
public Car(Handle handle, Pedal pedal, Sheet sheet) {
this.handle = handle;
this.pedal = pedal;
this.sheet = sheet;
}
public void drive() {
if (!(handle instanceof Handle)) {
throw new RuntimeException();
}
System.out.println("drive!!");
}
}
public class ListSubstituationPrinciple {
public static void main(String[] args) {
Handle handle = new Handle();
SportHandle sportHandle = new SportHandle();
Pedal pedal = new Pedal();
Sheet sheet = new Sheet();
Car car = new Car(handle, pedal, sheet);
car.drive();
car = new SportCar(sportHandle, pedal, sheet);
car.drive(); // exception 발생 이러면 정상적으로 동자을 하게끔 수정하던가 아예 상속을 받지 말아야함
}
}
이렇게 된 경우 SportCar 인스턴스로 재정의 된 car는 drive() 메소드를 수행하는데 있어 예외가 발생합니다.
리스코프 치환원칙에 따르면 해당 문제를 해결을 해야 하던가 SportCar 혹은 SportHandle이 상속을 받지 말아야 합니다.
Interface Segregation Principle, 인터페이스 분리의 원칙
인터페이스 분리 원칙은 하나의 일반적인 인터페이스 보다 여러개 구체적인 인터페이스를 구현해야 한다는 원칙입니다.
이를 위해선 인터페이스는 최대한 작고 구체적인 것을 여러개로 나누어서 사용해야 합니다.
예를 들어 Animal이라는 인터페이스가 있고 이를 Dog, Cat에서 각각 구현한다는 가정을 해봅시다.
public interface Animal {
public void 짖다();
public void 산책();
}
class Dog implements Animal {
@Override
public void 짖다() {
System.out.println("왈왈");
}
@Override
public void 산책() {
System.out.println("뚜벅");
}
}
class Cat implements Animal {
@Override
public void 짖다() {
System.out.println("냐용");
}
@Override
public void 산책() {
// 이를 수행할 수 없다면 이 인터페이스를 구현 하면 안되고
// 인터페이스를 더 세분화 해야함.
}
}
Animal 인터페이스는 짖다, 산책이라는 추상 메소드가 있습니다.
하지만 고양이는 산책을 할 수 없습니다.
일단 구현을 위해서 산책이라는 메소드를 마치 구현된것처럼 해놓았지만
이렇게 되면 큰 문제를 야기할 수 있습니다.
인터페이스는 이를 구현한 클래스에선 해당 메소드를 필히 구현해야 한다는 일종의 규약입니다.
즉 이를 구현한 클래스는 해당 인터페이스의 있는 메소드가 구현되어있다는 것을 보장해야합니다.
허나 이런 식으로 구현하게 되면 인터페이스의 의미가 없어지게 됩니다.
이를 해결하기 위해선
산책을 따로 담고 있는 인터페이스를 구현함으로써 Animal 인터페이스를 분리해야 합니다.
package week1.single;
public interface Animal {
public void 짖다();
}
interface WalkableAnimal extends Animal {
public void 산책();
}
class Dog implements WalkableAnimal {
@Override
public void 짖다() {
System.out.println("왈왈");
}
@Override
public void 산책() {
System.out.println("뚜벅");
}
}
class Cat implements Animal {
@Override
public void 짖다() {
System.out.println("냐용");
}
}
위와 같이 Animal을 WalkableAnimal로 분리하면서 짖는 것과 산책이 가능한 Dog, 짖는 것만 가능한 Cat 클래스에 대하여 구현하고
모두 인터페이스의 보장성을 채울 수 있습니다.
Dependency Inversion Principle, 의존 역전의 원칙
의존 역전의 원칙은 의존 관계를 맺을 때는 좀 더 일반적이고 추상적인 것에 의존해야한다는 원칙입니다.
만약 자신이 사용하려는 클래스가 상속하고 있는 클래스가 존재한다면 그 부모 클래스를 사용하는 것이 의존 역전의 원칙입니다.
만약 Rabbit이라는 클래스가 있고 해당 클래스는 당근이라는 클래스를 필드로 갖고 있습니다.
class Rabbit {
Carrot food;
}
class Carrot extends Vegtable {
}
의존성?
여기서 일단 의존성이라는 것을 알아야 합니다.
의존성이란, 한 객체가 다른 객체를 어떤 방식으로던지 참조한다는 것을 의미합니다.
현재 Rabbit 클래스는 Carrot을 필드로 갖고 있기 때문에 Rabbit은 Carrot 클래스에 의존하는 것이 될 수 있습니다.
이렇게 되면 Rabbit 클래스는 Carrot 클래스가 없으면 실행이 안되게 됩니다.
의존성이 강해지면 결합도가 강해지고 결합도가 강해지면 리팩토링 부터 테스트 등 여러 부분에서의 단점으로 작용되게 됩니다.
이를 대변 하는 원칙이 바로 의존 역전의 원칙입니다.
class Rabbit {
Vegtable food;
}
class Vegtable {
}
class Carrot extends Vegtable {
}
class Apple extends Vegtable {
}
만약 Rabbit의 클래스가 이제 Carrot이 아닌 Apple을 가져야한다고 가정합시다.
이렇게 되면 Rabbit 클래스를 또 수정해야 하는 상황이 발생합니다.
이보단 Apple, Carrot의 더 상위 개념인 Vegtable 클래스를 만들고
Apple, Carrot에선 이를 상속받게 합니다.
이후 Rabbit의 foot의 클래스를 Vegtable로 변경하게 되면
앞으로도 새로운 Vegtable 클래스가 생성되어도 유동적으로 이를 활용할 수 있게 됩니다.
이번엔 OOP의 가장 기본 적인 원칙인 SOLID에서 알아봤습니다.
애플리케이션 설계를 위해선 내가 사용하려는 클래스의 특징에 따라서 SOLID를 적용하며 하게 된다면
보다 수월한 개발이 이루어질 것입니다.