본문 바로가기
Java

객체지향 SOLID원칙

by 자유코딩 2020. 3. 18.
  • S = Single Responsibility Principle = 단일 책임 원칙
  • O = Open-Closed Principle = 개방 폐쇄 원칙
  • L = Liskov Substitution Principle = 리스코프 치환 원칙
  • I = Interface Segregation Principle = 인터페이스 분리의 원칙
  • D = Dependency Inversion Principle = 의존관계 역전의 원칙

SRP(Single Responsibility Principle) 단일 책임 원칙 - 클래스는 오직 책임이 하나여야 한다. 요구사항이 변경 되었을때는 변경 요인이 하나여야 한다.

하나의 클래스, 하나의 함수는 하나의 기능만을 수행하도록 개발되어야 한다.

 

하나의 클래스가 여러가지 동작을 수행한다면 코드를 수정하기 어렵다.

 

OCP(Open-Closed Principle) 개방 폐쇄의 원칙 - 클래스는 확장에는 열려 있지만, 수정에는 닫혀 있어야 한다.

기능을 변경, 확장할 수는 있지만 그 기능을 쓰는 코드는 수정하지 말아야 한다.

 

코드를 통해서 살펴보겠다. 아래와 같이 동작하는 코드가 있다고 해보자.

public class Main {
  public static void main(String[] args) {
    
  }
}

public class UseJava {
  public void hello(){
    System.out.Println("java");
  }
}

여기서 만약에 파이썬을 사용하려고 한다면 코드를 아마도 이렇게 바꿔야 할 것이다.

public class Main {
  public static void main(String[] args) {
    
  }
}

public class UsePython {
  public void hello(){
    System.out.Println("python");
  }
}

다시 자바를 쓰도록 바꾼다면 코드를 아래와 같이 다시 바꿔야 할 것이다.

public class Main {
  public static void main(String[] args) {
    
  }
}

public class UseJava {
  public void hello(){
    System.out.Println("java");
  }
}

바뀔때마다 이렇게 계속 코드를 고치는 것은 불편하다.

인터페이스를 사용해서 개방 폐쇄 원칙을 지킬 수 있다.

아래 코드처럼 말이다.

public class Main {
  UseLanguage useLanguage = new UseJava();
  public static void main(String[] args) {
    
  }
}

public interface UseLanguage {
	public void hello();
}

public class UseJava implements UseLanguage {
  public void hello(){
    System.out.Println("java");
  }
}


public class UsePython implements UseLanguage {
  public void hello(){
    System.out.Println("java");
  }
}

필요에 따라서 interface에 객체를 다르게 생성해서 사용 할 수 있다.

 

클래스의 개방 폐쇄 원칙

 

인터페이스에서만 이렇게 사용 하는 것이 아니고 클래스에서도 쓸 수 있다.

상위 클래스에 메소드를 정의하고 하위 클래스에서 상세 구현을 하면서 확장 할 수 있다.

 

리스코프 치환 원칙 - 서브 클래스는 수퍼클래스가 사용 되는 곳에 대체 될 수 있어야 한다.

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

코드를 통해서 살펴본다.

public class Main {
  public static void main(String[] args) {
    IpsMonitor monitor = new IpsMonitor();
    monitor.setTouch(false);
    monitor.isTouch(); // return false
    
    IpsMonitor ipad = new Ipad();
    ipad.setTouch(false);
    ipad.isTouch(); // return true -> false 기능을 의도했지만 ipad 와 모니터는 다르기 때문에
    // 상속으로 구현 할 수 없다. -> 하위 클래스인 아이패드가 상위 클래스인 모니터를 치환 할 수 없다.
  }
}

public class Ipad extends IpsMonitor{
  public boolean touch;
  public void setTouch(boolean touch) {
    this.touch = touch;
  }
  @Override  
  public boolean isTouch(){
    return true;
  }
}

public class IpsMonitor {
  public boolean touch;
  
  public void setTouch(boolean touch) {
    this.touch = touch;
  }
  
  public boolean isTouch() {
    return this.touch;
  }
}

코드를 보면 ips monitor 클래스는 touch 속성을 false 로 가진다. 이때 ipad 클래스를 정의한다.

ipad 클래스도 모니터라고 생각해서 ips monitor 로부터 상속 받아서 구현했다고 해봤다.

이렇게 되면 상위 클래스에서 의도한 touch 속성에 대한 값이 ipad 클래스에서는 다르게 된다.

 

처음부터 ips monitor 와 ipad 는 상위, 하위 클래스로 구현 되면 안됐다.

리스코프 치환 원칙을 어기고 구현을 했기 때문에 하위 클래스인 ipad 를 사용했을때 상위 클래스인 ips monitor 에서 의도한 것과 다르게 동작한다.

 

상위 클래스에서 정의한 명세대로 구현되고 하위 타입의 객체로 치환해도 정상적으로 동작해야 한다.

이게 리스코프 치환 원칙이다.

 

인터페이스 분리의 원칙 - 인터페이스는 구현 스펙을 정의한다. 이를 분리하라는 것은 거대한 클래스가 있다면 그것을 쪼개라는 것이다.

사용하지 않는 기능을 오버라이드 해서 구현 하면 안 된다.

코드를 통해서 살펴본다.

public class Main { 
    public static void main(String[] args) {
    
    }
}

public interface Utility {
    public void keyValue();
    public void minusIndex();
}

public class UseJava implements Utility{
    @Override
    public void keyValue() {
      System.out.Println("Map");
    }
    @Override
    public void minusIndex() {
      System.out.Println("last item");
    }
}

public class UsePython implements Utility {
    @Override
    public void keyValue() {
      System.out.Println("dict");
    }
    @Override
    public void minusIndex() {
      System.out.Println("last item");
    }
}

코드는 프로그래밍 언어를 쓰는 상황을 가정했다. 

코드에서는 utility 인터페이스를 사용해서 minusIndex 함수를 정의하고 있다.

UseJava 와 UsePython 클래스에서는 Utility 인터페이스를 구현 하고 있기때문에 minusIndex 함수를 구현해야 한다.

그런데 자바에서는 배열에 -1 인덱스를 쓸 수 없다.

코드를 맞게 고쳐보겠다.

 

public class Main { 
    public static void main(String[] args) {
    
    }
}

public interface Utility {
    public void keyValue();
}
public interface MinusIndex {
    public void minusIndex();
}

public class UseJava implements Utility{
    @Override
    public void keyValue() {
      System.out.Println("Map");
    }
}

public class UsePython implements Utility, MinusIndex {
    @Override
    public void keyValue() {
      System.out.Println("dict");
    }
    @Override
    public void minusIndex() {
      System.out.Println("last item");
    }
}

인터페이스를 2개로 나누었다. 클래스도 너무 크다면 나눠서 구현한다.

 

DIP(Dependency Inversion Policy) 의존 역전 원칙 - 구현에 의존하기 보다는 인터페이스에 의존하도록 코딩한다.

고 수준 모듈은 저 수준 모듈의 구현에 의존해서는 안된다

저 수준 모듈이 고 수준 모듈에서 정의한 추상 타입에 의존해야 한다.

 

코드로 예를 들면 아래와 같은 코드는 DIP 에 위배된다.

public class HighLevel {
  LowLevel low = new LowLevel();  
  public static void use() {
    low.hello();
  }
}

public class LowLevel {
  public static void hello() {
    System.out.Println("hello");
  }
}

HighLevel 클래스에서 LowLevel 클래스의 구현을 그대로  사용하고 있다.

이것을 인터페이스에 의존하도록 바꿔야 한다.

public class HighLevel {
  LowInterface low = new LowLevel();  
  public static void use() {
    low.hello();
  }
}

public interface LowInterface {
	public void hello();
}

public class LowLevel implements LowInterface {
  public static void hello() {
    System.out.Println("hello");
  }
}

LowInterface 인터페이스를 선언했다.

그리고 LowLevel 클래스에서 이것을 구현하도록 했다.

 

고수준인 HighLevel에서는 LowInterface 인터페이스에 LowLevel 클래스의 객체를 대입했다.

이렇게 하면 인터페이스를 사용하도록 구현 할 수 있다.

댓글