SOLID 원칙


단일 책임 원칙(SRP)

소프트웨어 설게 첫 번째 원칙으로 single responsibility principle 라는 단일 책임 원칙이 있다. 말 그대로 단 하나의 책임만을 가져야 한다는 의미이다.

책임

책임은 보통 ‘해야 하는 것’, ‘할 수 있는 것’ 으로 간주할 수 있다. 예를 들어 학생 클래스가 수강 과목을 조회, 데이터베이스에 정보 저장, 성적표 출력 등 여러 가지 작업을 수행한다고 하면 많은 책임을 지게 되는 것이다. 즉 클래스마다 잘 할 수 있는 일을 책임으로 부여하면 된다.

변경

설계 원칙을 배우는 이유는 예측하지 못한 변경사항이 발생하더라도 유연하고 확장성이 있도록 시스템 구조를 설계하기 위해서다. 위에서 언급한 책임은 곧 변경 이유가 된다. 또한 책임을 많이 질 수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.

책임 분리

위의 학생 클래스는 여러 책임ㅇ르 수행하므로 학생 클래스의 도움을 필요로 하는 코드가 많아질 수 밖에 없다. 이런 이유 때문에 학생 클래스에 변경사항이 생기면 학생 클래스를 사용하는 코드와 관계가 없더라도 직간접적으로 연관이 되어 있을 수 있으므로 모든 코드를 다시 테스트해야 한다. 참고로 어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 테스트를 회귀 테스트라고 한다. 이 테스트 비용을 줄이는 방법 하나는 시스템에 변경 사항이 발생했을 때 영향을 받는 부분을 적게 하는 것이다.

즉 한 클래스에 너무 많은 책임을 부여하지 말고 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 있는 것을 하나로 만들어야 하는데, 이를 책임분리라고 한다.

산탄총 수술

하나의 책임이 여러 개의 클래스들로 분산되어 있는 경우에도 단일 책임 원칙에 입각해 설계를 바꿔야 하는 경우에가 있는데, 이럴때 산탄총 수술이라는 말을 쓴다. 산탄총은 총을 쐈을 때 산탄이 사방으로 퍼지는데, 이 처럼 어떤 변경이 있을 때 하나가 아닌 여러 클래스를 변경해야 한다는 점에 착안해 만들어졌다.

이를 해결하는 방법은 클래스마다 있는 부가 기능을 별개의 클래스로 분리해 책임을 담당하게 하는 것이다. 즉 여러 곳에 흩어진 공통 책임을 한 곳에 모으면서 응집도를 높인다. 하지만 이렇게 독립 클래스를 구현해도 구현된 기능들을 호출하고 사용하는 코드도 클래스마다 여러 곳에 흩어져 있다.

관심지향 프로그래밍과 횡단 관심 문제

이런 횡단 관심 문제를 해결하는 방법으로 관심지향 프로그래밍(Aspect-Oriented Programming, AOP)기법이 있다. AOP는 횡단 관심을 수행하는 코드를 Aspect라는 특별한 객체로 모듈화하고 Weaving이라는 작업을 통해 모듈화한 코드를 핵심 기능에 끼워넣을 수 있다. 이를 통해 기존 코드를 변경하지 않고도 시스템 핵심 기능에서 필요한 부가 기능을 효과적으로 이용할 수 있다. 횡단 관심이 변경된다면 해당 aspect만 수정하면 되는 것이다.

아래는 AOP와 관련된 용어들이다.

  • 조인포인트 : 애플리케이션 실행 중의 특정한 지점을 말한다. 메서드 호출, 메서드 실행 자체, 클래스 초기화 시점 등이 있다. 애플리케이션의 어떤 지점에서 AOP를 사용해 추가적인 로직을 삽입할지를 정의한다.
  • 어드바이스 : 특정 조인포인트에 실행하는 코드를 말한다.
  • 포인트컷 : 여러 조인포인트의 집합체로, 언제 어드바이스를 실행할지 정의할 때 사용한다.
  • 애스팩트 : 어드바이스와 포인트 컷을 조합한 조합물이다.
  • 위빙 : 애플리케이션 코드의 해당 지점에 애스펙트를 실제로 주입하는 것을 말한다.

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

계방-폐쇄 원칙은 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻이다. OCP를 위반하지 않는 설계를 할 때 가장 중요한 것은 무엇이 변하는 것인지, 무엇이 변하지 않는 것인지 구분해야 한다는 것이다. 변해야 하는 것은 쉽게 변할 수 있게 하고, 변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 한다. 또 다른 관점은 클래스를 변경하지 않고도(closed) 대상 클래스의 환경을 변경할 수 있는(open) 설계가 되어야 한다는 것이다. 이거는 특히 단위 테스트를 할 때 매우 중요하다. 예를 들어 Mock 객체를 만들어서 테스트를 하는 것처럼, 실제 서비스에서 사용할 객체를 그대로 테스트하면 위험할 수도 있다. 이 때문에 테스트를 위해 가짜 객체를 만들 필요가 있다.

참고로 모의 객체는 테스트 더블의 한 종류로, 무언가를 대신하는 가짜라는 뜻이다.

  • 더미 객체 : 테스트할 때 객체만 필요하고 해당 객체의 기능까지는 필요하지 않은 경우에 사용한다.
  • 테스트 스텁 : 더미 객체에 단순한 기능을 추가한다.
  • 테스트 스파이 : 테스트 대상 클래스가 의존하는 클래스로의 출력을 검증하는데 사용한다.
  • 가짜 객체 : 실제 의존 클래스의 기능을 대체해야 할 경우에 사용하고, 실제 의존 클래스의 기능 중 전체나 일부를 훨씬 단순하게 구현한다.
  • 목 객체 : 미리 정의한 기대 값과 실제 호출을 단언문으로 비교해 문제가 있으면 테스트 메서드를 대신해 모의 객체가 테스트를 실패하게 한다.

리스코프 치환 원칙(Lskov Substitution Principle, LSP)

LSP는 일반화 관계에 대한 이야기이고 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 뜻이다. LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변하지 않는다. 즉 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 말이다.

부모와 자식 클래스의 행위가 일관성이 있으려면 부모 클래스의 행위를 더 명확하게 정의할 수 있는 수단이 필요하다. 이는 어떤 클래스의 행위를 일종의 방정식 형태로 기술해 자식 클래스의 인스턴스가 이 방정식을 만족하는지 점검할 수 있다.

public class Bag {
  private int price;
  
  public void setPrice(int price) {
    this.price = price;
  }
  
  public int getPrice() {
    return price;
  }
}

이 클래스의 행위는 가격은 설정된 가격 그대로 조회된다 라고 말할 수 있다. 이를 다음과 같이 표현할 수 있다.

[b.setPrice(p)].getPrice() == p 여기에서 [객체.메서드(인자리스트)]는 메서드가 싫행된 후의 b객체를 의미한다. 이런 Bag클래스의 행위를 일관성 있게 실행하는 클래스를 만드려면 슈퍼 클래스에서 상속받은 메서드들이 서브 클래스에 오버라이드, 재정의 되지 않으면 된다. Bag 클래스를 상속받은 DiscountedBag라는 클래스가 있다고 해보자.

public class DiscountBag extends Bag {
  private double discountedRate = 0;
  
  public void setDiscounted(double discountedRate) {
    this.discountedRate = discountedRate;
  }
  
  public void applyDiscount(int price) {
    super.setPrice(price - (int)(discountedRate * price));
  }
}

DiscountedBag 클래스에서 setPricegetPrice를 재정의하지 않았으므로 DiscountedBag 클래스의 인스턴스가 이를 실행시켜도 실행 결과는 동일하며, 이는 LSP를 위반하지 않는다는 것을 의미한다. 앞에서도 봤었지만 피터코드의 상속 규칙에서는 서브 클래스가 슈퍼 클래스의 책임을 무시하거나 재정의 하지 않고 확장만 수행한다고 했는데, 이는 슈퍼 클래스의 메서드를 오버라이딩하지 않는 것과 같은 의미다. 즉 피터 코드의 상속 규칙을 지키면 LSP를 지킬 수 있다.

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

객체 사이에 도움을 주고 받으면 의존 관계가 생긴다. DIP는 의존 관계를 맺을 때 가이드를 준다. DIP는 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칠기아.

추상적인 것은 변경하기 어려운 것에, 구체적인 것은 변하기 쉬운 것으로 구분하면 좋다. DIP를 만족하려면 어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 인터페이스나 추상 클래스와 의존 관계를 맺도록 설계해야 한다. 이런 설계를 만족하면 변화에 유연한 시스템이 된다.

DIP를 만족하면 의존성 주입(dependency injection)을 이용하여 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다. 의존성 주입이란 클래스 외부에서 의존되는 것을 대상 객체의 인스턴스 변수에 주입하는 기술이다. 이를 사용하면 대상 객체를 변경하지 않고도 외부에서 대상 객체의 외부 의존 객체를 바꿀 수 있다.

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

ISP는 예의 관점(클라이언트 관점에서 바라는)에서 생긴 객체지향 설계 원칙에는 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이 담겨 있다. 즉 각각의 클라이언트가 원하는 기능을 모두 담은 클래스를 만들기 보다는 각각의 클라이언트가 원하는 기능 각각에 특화된 인터페이스를 사용해야 한다. 즉 인터페이스를 클라이언트에 특화되도록 분리하는 설계원칙을 의미한다.

SRP와 같이 생각해봤을 때, 어떤 클래스가 여러 책임을 수행하면 다양한 메서드를 가지게 될 것이고, 이에 따라 비대한 인터페이스가 제공될 것이다. 이를 SRP에 따라 단일 책임을 갖는 여러 클래스로 분할하면 ISP를 만족할 수 있다. 하지만 꼭 그렇다고만 할 수 없다. 게시판 클래스가 있고, CRUD 기능이 있는데 클라이언트에 따라 이 기능을 일부만 사용할 수 있다. 이럴 경우 SRP는 만족하지만 클래스의 모든 메서드가 들어 있는 인터페이스가 클라이언트에 상관없이 사용된다면 ISP에 위반된다.

sdfsdfasdfasdfasdfals;kdjf;aolekrn