객체 지향 5대 원칙 - 2

김태영·2024년 6월 12일
0

TIL

목록 보기
27/70
post-thumbnail

오늘 공부한 것

- 알고리즘 예상 대진표 풀이
- 문법 특강 복습
- 개인 과제 구조 설계

객체 지향 5대 원칙

  1. 단일 책임 원칙 (Single Responsibility Principle)
  2. 개방 폐쇄 원칙 (Open Closed Principle)
  3. 리스코프 치환 원칙 (Liscov Substitution Principle)
  4. 인터페이스 분리 원칙 (Interface Sergregation Principle)
  5. 의존성 역전 원칙 (Dependency Inversion Principle)

지난 번 정리에 이어 오늘은 개방 폐쇄 원칙과 리스코프 치환 원칙, 인터페이스 분리 원칙에 대해 알아보았다.

개방 폐쇄 원칙

사실 이전에 결합도가 낮은 클래스를 설명할 때, 개방 폐쇄의 원칙을 잘 지킨 클래스라고 했었다. 오늘은 이 원칙에 대해 조금 더 알아보려고 한다.

개방 폐쇄 원칙은 확장에 대해 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다는 원칙이다.

  • 확장에 대해 열려있다 → 새로운 변경 사항이 발생했을 때, 새로운 동작을 추가하여 쉽게 기능을 확장할 수 있다.
  • 수정에 대해 닫혀있다 → 새로운 변경 사항이 발생했을 때, 기존의 코드를 수정하지 않고 동작을 추가하거나 변경할 수 있다.

추상화에 의존하면 어렵지 않게 구현할 수 있다고 한다. 변하지 않는 부분만 남겨놓고, 변하는 부분은 생략하여 추상화 한다면 나중에 생략된 부분을 수정하는 방식으로 개방 폐쇄 원칙을 지킬 수 있다고 한다!

예시

그럼 맥X날드의 햄버거로 예시를 들어보자.

  • 카드로 결제하는 기능을 CardPayment 클래스에 이미 구현해놓았다.
  • 구현해놓은 결제 함수를 맥X날드 클래스에서 불러와 결제 기능도 구현해놓았다.
  • 그런데 토스 페이로 구매하게 변경해달라는 요청을 받았다.
  • 나는 맥X날드 클래스와 CardPayment 클래스를 모두 수정 해서 토스 페이로 결제하는 기능을 구현했다.

위와 같은 경우는 개방 폐쇄 원칙을 과연 지켰을까? 결제 방식만 바꾸면 되는데 맥X날드 클래스까지 수정했기 때문에 원칙을 지키지 못했다고 할 수 있다. 그럼 어떻게 해야 됐을까?

카드로 “결제”하고, 토스 페이로 “결제”한다. 어떤 방식을 사용하던 “결제”를 한다는 것은 동일하므로, 우리는 Payment란 클래스 안에 pay라는 메소드를 일단 선언해놓고 보는 것이다.

abstract class Payment {
	fun pay()
}

그 이후에 이 Payment 클래스를 구체화해서 카드 결제로 구현하던, 토스 페이로 구현하던 맥X날드 클래스에서는 Payment().pay()를 사용하면 된다! 즉, 맥X날드 클래스의 수정 없이 기능 추가가 가능하다. 이 경우는 개방 폐쇄 원칙을 잘 지켰다고 할 수 있다.

핵심은 추상화를 통해 “변하지 않는 것들에 의존”해야 한다는 것임을 잊지 말자. 반대로 변하는 것들은 생략하면 된다!

리스코프 치환 원칙

하위 타입은 상위 타입을 대체할 수 있어야 한다는 원칙이다. 즉, 상속 관계에서 자식 타입은 부모 타입으로 교체할 수 있어야 한다는 뜻이다. 이 문장을 읽으면 바로 다형성이 생각날 것이다. 리스코프 치환 원칙은 다형성을 이용하기 위한 원칙이라고 한다.

예시

일반 회원 가격과 멤버십 회원 가격이 다른 상품을 파는 주얼리 사이트로 예를 들어보자.

  • 일반 회원 가격과 멤버십 회원 가격을 갖는 상품 클래스가 있다.
  • 이 상품 클래스를 상속 받아 목걸이 클래스, 반지 클래스를 만들었다.
  • 그런데 반지는 특별 상품이라 일반 회원 가격과 멤버십 회원 가격이 동일하다고 하자.
    • 이걸 구현하기 위해 매개변수 2개를 입력 받되, 하나의 매개변수로 일반 회원과 멤버십 회원의 가격을 설정해주었다.

지금까지 작성된 클래스는 다음과 같다.

abstract class Product() {
    var generalPrice: Int = 0
    var membershipPrice: Int = 0

    abstract fun setPrice(general: Int, membership: Int)
}

class Ring() : Product() {
    override fun setPrice(general: Int, membership: Int) {
		    // general로 일반 회원과 멤버십 회원의 가격을 설정
        super.generalPrice = general
        super.membershipPrice = general
    }
}

class Necklace() : Product() {
    override fun setPrice(general: Int, membership: Int) {
        super.generalPrice = general
        super.membershipPrice = membership
    }
}

이렇게 구현한 뒤, 다른 사람이 상품들의 가격을 타입 상관 없이 조정하기 위해 다음과 같은 함수를 만들었다고 하자.

fun changePrice(item: Product, newGeneralPrice: Int, newMembershipPrice: Int) {
    item.setPrice(newGeneralPrice, newMembershipPrice)
}

이걸 구현한 사람은 반지 클래스도 일단은 상품이니까 일반 회원 가격과 멤버십 회원 가격을 각각 설정해주고 싶었다. 따라서 반지 인스턴스를 만들어 다음과 같이 changePrice를 호출하게 된다.

changePrice(ring, 1400, 1000)

그러나 코드는 의도한 대로 동작하지 않고, 일반 회원 가격과 멤버십 회원 가격 모두 1400원으로 설정되는 현상이 발생한다.

설명을 위해 살짝 무지성으로 예를 들어서 좀 이상할 수도 있지만, 이런 상황과 같이 자식 클래스는 부모 클래스가 의도한 동작에서 벗어나면 안된다. 이 예시에서 부모 클래스가 의도한 동작은, 값 2개를 받아 각각 일반 회원 가격, 멤버십 회원 가격에 설정해주는 것이다. 그러나 반지 클래스는 이를 어기고 값 하나로 모든 가격을 설정해주었다.

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체할 수 있어야 하는 원칙이기 때문에, 예시와 같은 상황이 발생하지 않게 작성해야 원칙을 지킬 수 있다.

인터페이스 분리 원칙

인터페이스를 각각 용도에 맞게 잘게 분리해야한다는 원칙이다. 인터페이스는 다중 상속이 가능하기 때문에 하나의 인터페이스에 많은 기능을 넣는 것보다, 목적과 용도에 맞게 적은 기능으로 분리하는 것이 인터페이스 분리 원칙을 지킬 수 있는 방법이다.

마치며

원칙이 5개나 있는데 2개만 해놓고 나몰라라 하는 건 좀 아닌 것 같아 마저 정리해보았다. 상속은 어떤 때에 사용하는 것인지 대충은 알 것 같다. 정말 대충..

profile
화이팅

0개의 댓글