디자인 패턴/객체지향 프로그래밍

객체지향 프로그래밍2

JunsuKim 2022. 10. 4.
728x90

SOLID

객체지향 설계 원리 중 가장 중요한 5가지가 있는데, 이 5가지를 합쳐 SOLID라고 한다.

  • Single Reponsibility Principle: 클래스의 응집성이 높아야 한다.
  • Open-Closed Principle: 코드 수정없이 확장이 가능해야 한다.
  • Liskov Substitution Principle: 상위 타입은 항상 하위 타입으로 교체가 가능해야 한다.
  • Interface Segregation Principle: 필요없는 것을 구현하도록 강요하지 않아야 한다. 즉, interface의 덩치가 작아야 한다.
  • Dependency Inversion Principle: 클래스는 구체적 클래스 대신에 상위 추상 타입이나 interface에 의존해야 한다.

SRP(Single Reponsibility Principle)

  • 클래스는 응집성이 높아야 한다.
  • 클래스는 여러 멤버 변수를 유지하고 여러 메소드를 가질 수 있지만, 이들은 모두 한 가지 책임을 위해 존재해야 한다.

위 사진을 보면 Employee 클래스는 CalculatePay(), WriteToDB() 두 가지 메소드를 가지고 있다.

즉, Employee 클래스는 두 가지 책임을 맡고 있다. 따라서 둘 중 더 필요한 책임을 Employee 클래스에 놓고, 나머지는 별도 클래스로 만든다. 

OCP(Open-Closed Principle)

  • 코드 수정없이 클래스를 확장하는 기본 방법
  • 방법1. 상속
  • 방법2. 포함 관계 활용
    • 기존 클래스가 제공하는 기능은 이 클래스의 객체를 멤버 변수로 유지하여 사용하고 추가로 필요한 기능만 구현하여 새 클래스를 정의한다.
    • 이때 상위 타입의 참조 변수에 유지하면 더 유연한 코드가 된다.
      • 유지하는 구체적인 객체를 동적으로 변경할 수 있다.

 LSP(Liskov Substitution Principle)

  • Subclassing을 하더라도 subtyping이 충족되어야 한다는 것이 LSP이다.
  • LSP가 충족되기 위해 메소드를 재정의할 때 보장되어야 하는 것
    • 메소드 매개변수 타입의 반변성
    • 메소드 반환타입의 공변성
    • 자식 메소드가 더 많은 종류의 예외를 발생할 수 없다.
  • 공변성은 더 특수화하는 것이고, 반변성은 더 일반화되는 것을 말한다.
    • 매개변수 타입에 대해서 정확한 일치를 요구한다.
  • LSP는 문법적인 측면뿐만 아니라 논리저인 측면에서 다음이 보장되어야 한다.
    • 메소드의 사전 조건은 강화되지 않아야 한다.
    • 메소드의 사후 조건은 약하되지 않아야 한다.
    • 상위 타입의 불변 조건은 계속 유지되어야 한다.
    • (히스토리 규칙) 객체는 자신의 메소드를 통해서만 상태가 변경될 수 있어야 한다.
    • 사전 조건: n<0, 사후 조건: retval > 0
    • 사전 조건 강화의 경우: n > 0, n % 2 == 0
    • 사후 조건 약화의 경우: retval > -1000
  • 상속할 때 자식 클래스에 새 메소드를 추가하면 LSP 위배일까?
    • LSP는 부모 리모컨을 사용하여 객체를 처리할 때 문법적으로, 논리적으로 문제가 없어야 한다.
    • 자바에서 interface를 구체화한 클래스는 보통 interface에 선언된 메소드 외에 추가적인 공개 메소드를 가지고 있다.
    • 자식 클래스에 새 메소드의 추가는 LSP와 무관하다.
      • 하지만 부모에 없는 공개 메소드의 추가는 객체지향 설계 측면에서 바람직한 것은 아니다.
    • LSP의 핵심은 부모 공개 메소드의 재정의이다.
      • 빈 메소드로 재정의하는 것은 LSP에 위배되는 것이다.

LSP가 위배된 경우 해결 방법

  • 부모 자식 간이 아니라 형제 클래스로 모델링한다.
  • 자식 중 특정 메소드가 필요한 쪽과 그렇지 않은 쪽이 있으면 해당 메소드를 부모 타입에서 제외하고, 그것을 포함하는 하위 클래스를 추가한다.
  • 포함 관계를 활용한다.

ISP(Interface Segregation Principle)

  • 클래스나 interface가 제공하는 메소드의 수는 최소화되어야 한다.
  • 단일 메소드 interface를 사용하면 ISP가 위배될 수 없다.
  • 항상 같이 제공되어야 하는 것까지 분리할 필요는 없다.
  • ISP의 interface는 자바의 interface를 말하는 것이 아니다.
    -> 클래스나 interface의 공개 메소드의 집합을 말한다.
public interface LivingThing {
    void eat();
    void walk();
    void swim();
    void fly();
}

위 코드는 잘못 설계된 인터페이스이다.

살아있는 모든 것들이 먹고, 걷고 수영하고, 날고 하는 것이 아니기 때문이다.

ISP를 적용시키면 다음과 같이 된다.

public interface LivingThing {
    void eat();
}

public interface LivingSky extends LivingThing {
    void fly();
}

public interface LivingOnLand extends LivingThing {
    void walk();
}

public interface LivingInWater extends LivingThing {
    void swim();
}

DIP(Dependency Inversion Principle)

  • 의존 관계의 결합 정도(느슨한 결합 vs 강한 결합)와 관련된 원리이다.
  • 기본적으로 의존 관계가 적을수록 좋다. -> 느슨한 결합이 좋다.
  • DIP는 의존을 하더라도 구체적인 클래스 대신에 상위 추상 타입이나 interface에 의존해야 한다는 원리
    • 의존 관계는 순환되지 않고, 일방향인 것이 더 바람직하다.
  • DIP 원리에 충실해야 OCP가 가능해진다.
class Person {
    private Dog dog;
    ....
};

위 코드를 보면 Person 클래스는 구체적인 Dog 클래스에 의존하고 있다.

Dog 클래스는 Person 클래스를 의존하지 않으므로 관계도를 그리면 다음과 같다.

구체적인 클래스에 의존하기 때문에 강아지가 아닌 고양이를 키우고 싶다면 코드 수정이 불가피하다.

class Person {
    private Pet pet;
    public Person(Pet pet) {
        setPet(pet);
    }
    public void setPet(Pet pet) {
        this.pet = pet;
    }
};

코드를 위와 같이 수정하면 Person 클래스는 더 이상 Dog 클래스에 의존하지 않는다.

setPet을 이용하여 언제든 다양한 동물을 유지할 수 있다.

이와 같은 방식을 관계 주입(dependency injection)이라 한다.

관계를 고정하는 것이 아닌 사용하는 측에서 관계를 동적으로 맺어주는 방식으로, 생성자 관계 주입 방식도 있고, setter를 이용하는 방식도 있다.

위 코드의 관계도는 다음과 같다.

관계 주입이 의존 관계를 뒤집는 유일한 방법은 아니다.

무엇을 할 것인지를 결정하는 부분과 언제 할 것인지를 결정하는 부분을 분리하면 의존 관계를 뒤집을 수 있다.

기타 객체지향 설계 원리

  • 변할 가능성이 높은 부분을 추상화한다.
    • OCP를 적용하기 위해 확장이 가능한 부분과 그렇지 않은 부분을 구분할 수 있어야 한다.
  • 구체적인 타입에 의존하는 것이 아닌 추상 타입에 의존해야 한다.
    • DIP에 포함되어 있는 원리
  • 느슨한 연결을 선호
  • 상속 관계보다 포함 관계를 선호하자.
    • OCP를 제공하는 방법에서 이 원리가 적용
    • 이 원리에 따라 상속을 절대 악으로 생각하는 것은 잘못이다.
  • 할리우드 원리
    • 클래스 간 의존 관계가 복잡하게 얽히고 꼬여있지 않아야 한다.
    • 하위 수준 요소는 상위 수준 요소와 연결되어 동작할 수 있지만 상위 수준 요소가 언제 어떻게 하위 수준 요소를 사용할지 결정하며, 하위 수준 요소는 상위 수준 요소를 직접 호출하지 않아야 한다.
    • 상위/하위 수준 요소는 상속 관계에 있는 요소로 제한되지 않는다.
    • DIP가 더 포괄적 개념이다.
      • DIP는 직접 의존 관계를 맺지 않아도 사용할 수 있다는 개념이다.
      • 이 원리는 상위/하위 수준 요소 간 관계와 상호작용 방법에 초점을 두는 원리이다.

Anti-Pattern

  • 코드에는 설계 패턴과 같은 좋은 패턴만 있는 것이 아니다.
  • 많은 코드에 자주 등장하는 나쁜 패턴을 안티 패턴이라고 한다.
  • 안티 패턴을 다른 말로는 Code smell이라고도 한다.
  • Code smell을 개선하기 위해 사용하는 기술이 리펙토링이다.
  • 리펙토링은 코드의 외부 행위가 유지된 상태에서 내부 코드 구조를 개선하는 작업이다.

Code Smells

  • 이해하기 힘든 이름
  • 주석
    • 나쁜 설계 결과 때문에 주석이 필요한 것이 아닌지?
    • 가독성이 떨어져 필요한 것은 아닌지?
  • 코드 중복
  • 긴 함수
  • 큰 클래스(블랙홀 클래스)
  • 너무 작은 클래스(데이터 클래스)
    • 이 객체를 사용하는 다른 객체를 검토하여 이 클래스에 추가적으로 제공해야 하는 기능이 있는지 검토할 필요가 있다.
  • 데이터 덩어리
  • 긴 매개변수 목록
  • 광역 데이터

코드 변경 관련 code smells

  • Divergent change: 한 모듈(함수, 클래스)이 변경해야 하는 이유가 여러 가지인 경우
  • Shotgun surgery: 한 곳의 변경이 여러 다른 곳의 변경까지 필요한 경우
  • Featrue envy: 한 클래스의 메소드가 자신의 상태보다 다른 클래스 상태에 더 관심이 많은 경우
  • Inappropriate Intimacy: A와 B 클래스가 서로 의존하는 경우
728x90

'디자인 패턴 > 객체지향 프로그래밍' 카테고리의 다른 글

객체지향 프로그래밍1  (1) 2022.10.04

댓글