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

객체지향 프로그래밍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)

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

객체지향 프로그래밍2 - undefined - 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 클래스를 의존하지 않으므로 관계도를 그리면 다음과 같다.

객체지향 프로그래밍2 - undefined - DIP(Dependency Inversion Principle)

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

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를 이용하는 방식도 있다.

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

객체지향 프로그래밍2 - undefined - DIP(Dependency Inversion Principle)

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

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

기타 객체지향 설계 원리

  • 변할 가능성이 높은 부분을 추상화한다.
    • 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

댓글