객체지향과 SOLID

이용구·2025년 1월 19일
post-thumbnail



누군가 객체지향 프로그래밍이 뭐냐고 물어본다면 객체를 지향하는 프로그램이라 말할 것이다.




.
.
.



다시 공부하도록 하자.




객체지향 특징 4가지

1. 추상화

2. 캡슐화

3. 상속

4. 다형성




1. 추상화 - 객체의 공통된 특징을 파악하여 상위 개념으로 정의

Q. 추상화가 뭐죠?
A. 객체의 공통된 특징을 모아서 추출하는 것이다.

Q. 객체의 공통된 특징을 모아서 추출하는 게 왜 추상이죠??
A. 사전적 의미를 확인해 보자

추상 : 여러 사물 또는 개념 따위의 개별자들로부터 공통점을 파악하고 추려내는 것


즉, 공통된 부분을 추려내어 정의한다고 생각하자.


예를 들어 싼타페, 쏘나타, 마티즈가 있다면
모두 디자인, 크기, 연비, 속도가 다르지만

모두 바퀴가 있고
문이 있으며
직진하는 기능을 한다.
등등 여러 기능이 있다.

공통인 것을 묶어 자동차라고 추상화한다.


여기서 설명을 추가하자면, 공통된 것을 묶어 자동차라고 추상화한다
-> 싼타페, 쏘나타, 마티즈를 자동차라고 상위 개념으로 정의한다.


이걸 객체로 다시 생각해보자.
쏘나타 클래스를 만들고, 마티즈 클래스를 만들고 등등..

이렇게 만들다보니. 바퀴, 문, 직진 기능이 공통된 것을 확인하였다.
그래서 공통된 것을 가지고 있는 Car라는 상위 개념의 클래스(or 인터페이스)로 정의한다.

-> ⭐️여러 객체(또는 방식, 개념 등)의 공통된 특징을 파악하여, 그것을 하나의 상위 개념으로 정의하는 것



이때 중요한 건 “구현을 숨기고, 개념(무엇을 한다)”에만 집중하는 것이다.
즉, "어떻게 한다"는 각 구현체에 맡기고, "무엇을 한다"는 것만 정의하는 것.

위 예시로 비유를 하자면,
공통된 상위 개념을 가진 Car 클래스로 추상화를 한다면.
직진을 한다(무엇을 한다)에만 집중을 하고
각 자동차별로 어떻게 직진을 하는지는 각 구현체에 맡긴다!



관련 용어 정리

  • 클래스 = 추상 자료형 (일반 클래스, 추상 클래스)

  • 객체 = 추상 자료형의 인스턴스

  • 메서드 = 추상 자료형에서 정의된 연산


클래스는 일반 클래스추상 클래스로 나뉜다.


추상 클래스(extends)는 그 클래스 내에 '추상 메서스'가 하나 이상 포함되거나 abstract로 정의된 경우이다.

반면 인터페이스(implements)는 모든 메서드가 '추상 메서드'인 경우이다.



그럼 여기서 궁금증이 생긴다. (생겨야 한다)


??아니 그럼 추상 클래스 안에 모든 메서드가 추상 메서드로 구현되어 있다면, 인터페이스는 굳이 필요 없는 거 아닌가?



>>결론적으로 목적이 다르다.

  • 추상 클래스는 그 추상 클래스를 상속받아서 기능을 이용하고, 확장시키는 데 있다.

  • 반면 인터페이스는 구현을 강제하는 것이 목적이다!

즉, 해당 인터페이스를 구현한 객체들에 대해서 동일한 동작을 약속하기 위해 존재한다.






2. 캡슐화 - 객체의 속성(데이터 필드)와 행위(메서드)를 하나로 결합

객체의 데이터 필드와 메서드를 한 번에 묶는다는 것을 의미한다.


그거 당연한 거 아닌가요? 거창하게 캡슐화라고 따로 부르는 이유가 뭐죠?



실제 구현 내용 일부를 외부에 은닉 (private, public, protected)


두 가지 의미를 가진다.

1. 데이터 보호
접근 지정자(private, public, protected)를 통해 외부에서 접근하지 못하도록 하는 역할

2. 데이터 은닉
get/set 함수를 통해서만 변수에 접근 가능하다.
변수는 모두 private, 함수만 public






3. 상속 - (재사용) 새로운 클래스가 기존의 클래스의 자료와 연산 이용 가능

기존의 클래스를 이용하여 새로운 클래스를 만든다.

상속은 슈퍼클래스의 기능을 이용하거나 확장하기 위해서 사용되고, 다중 상속의 모호성 때문에 하나만 상속 가능하다.






⭐4. 다형성 - 같은 형태 다른 기능

의미: 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질


ex) 라는 사람의 역할이 학교에서는 학생, 집에선 아들


인터페이스와 그 인터페이스를 구현한 객체를 통해 여러 가지 형태를 가진다.

-> ⭐️같은 메서드 호출이 상황(객체)에 따라 다르게 동작하는 것



관련 용어정리

오버 로딩 - 같은 이름의 메서드이지만 매개변수 개수나 데이터 타입을 다르게 정의하여 사용

오버라이딩 - 부모 클래스를 상속받아 자식 클래스에서 부모 클래스의 메서드를 재정의




Q. 그럼 다형성은 단순히 여러 가지 형태를 가진다는 게 핵심인가요??

다형성의 본질은 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 가능하다는 것이다!



운전자 입장에서 자동차 역할의 인터페이스만 알고 있다면
그 자동차의 구현이 소나타인지 산타페인지 상관없다.
운전자는 자동차 역할만 알면 된다. (직진, 후진, 핸들링)

그저 인터페이스만 바라보면 된다.
(이는 solid의 ocp, dip와 연결된다)


즉 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.



⭐중요한 어록

역할과 기능을 분리

역할 - 인터페이스

기능 - 인터페이스를 구현한 클래스, 구현 객체이다





추가적으로

스프링은 다형성을 극대화하여 이용할 수 있게 도와준다.


다시 말해

스프링을 사용하는 이유는 객체지향의 특징을 가장 극대화하여 사용할 수 있게 도와주는 프레임워크이기 때문이다.


스프링에서 제어의 역전(ioc), 의존관계 주입(di)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있게 지원한다.








좋은 객체지향 설계 원칙 5 SOLID

SOLID

1. SRP (Single Responsibility)

2. OCP (Open/Close)

3. LSP (The Liskov Substitution)

4. ISP (Interface Segregation)

5. DIP (Dependency Inversion)




1. SRP(Single Responsibility) 단일 책임 원칙 - 한 클래스는 하나의 책임만 가진다.

쉽게 말해, 변경 시 파급력이 적으면 SRP를 잘 지키게 설계했다고 판단.


// 잘못된 예
class User {
    public void login() {
        // 로그인 로직
    }

    public void save() {
        // db 저장 로직
    }
}

User클래스에는 로그인 로직과 db 저장 로직을 가지고 있다.



// SRP 적용
class User {
    // User 관련 데이터
}

class UserRepository {
    public void save(User user) {
        // db 저장 로직
    }
}

class UserService {
    public void login(User user) {
        // 로그인 로직
    }
}

UserServiceUserRepository을 통해 책임을 분리하였다.






⭐2. OCP(Open/Close) 개방 폐쇄 원칙 - 확장에는 열려있고 변경에는 닫혀있다.

(변경을 할 수 없는데 확장을 어떻게 하죠?)


다형성을 활용하여 가능하다.

인터페이스를 구현한 새로운 클래스를 만들어 새로운 기능 구현하여 해결 가능하다.



OCP 적용 전

public class DiscountService {

    public double discount(String customerType, double amount) {
        if (customerType.equals("Regular")) {
            return amount * 0.1; // Regular 10% 할인
        } else if (customerType.equals("VIP")) {
            return amount * 0.2; // VIP 20% 할인
        } else if (customerType.equals("New")) {
            return amount * 0.05; // 신규 5% 할인
        } else {
            return 0; // 할인 없음
        }
    }
}

할인 서비스를 예시로 들어보자.
DiscountServicediscount()메서드는 VVIP 고객 유형이 추가된다면,
또 코드를 추가해야 할 것이다.




OCP 적용 후

public interface DiscountPolicy {
    double discount(double amount);
}

DiscountPolicy 인터페이스를 만들어 각 할인 정책 인터페이스를 구현한다.



  // Regular 고객 할인 정책
  public class RegularDiscountPolicy implements DiscountPolicy {
      @Override
      public double discount(double amount) {
          return amount * 0.1;
      }
  }

  // VIP 고객 할인 정책
  public class VIPDiscountPolicy implements DiscountPolicy {
      @Override
      public double discount(double amount) {
          return amount * 0.2;
      }
  }

  // 신규 고객 할인 정책
  public class NewDiscountPolicy implements DiscountPolicy {
      @Override
      public double discount(double amount) {
          return amount * 0.05;
      }
  }

DiscountPolicy 인터페이스를 구현하는 새로운 클래스를 추가하기만 하면 된다.






3. LSP(The Liskov Substitution) 리스코브 치환 원칙 - 통상적으로 지키는 기능을 지킨다

자동차 인터페이스를 구현했다.
악셀이라는 기능을 구현했는데, 악셀은 직진이다.
그러나 악셀 기능이 뒤로 가게 동작한다면?
이 원칙을 위반했다.






4. ISP(Interface Segregation) 인터페이스 분리 원칙 - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다

즉, 하나의 일반적인 인테페이스보다는, 여러 개의 구체적인 인터페이스가 좋다.






⭐5. dip(Dependency Inversion) 의존관계 역전 원칙 - 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안 된다.


고수준의 모듈은 저수준 모듈의 구현에 의존하면 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다.



쉽게 이야기해서 클라이언트 코드가 구현 클래스를 바라보지 말고 인터페이스를 바라보아라

= 구현 클래스에 의존하지 말고 인터페이스만 의존해라

= 역할에 의존하게 해야 한다. (운전자 ---> 자동차 역할만을 바라본다. k3인지(구현)에 의존하면 안 된다)

⭐ "의존한다" 라는건 내가 저 코드를 알고있다는 것이다.



Q. 의존관계가 역전된다는 의미는 무엇을 말하는 건가요?


인터페이스가 없던 시점에서 운전자소타타 코드를 가지고 있다. (즉 소타타를 의존한다)


하지만 dip를 적용하면 자동차 역할구현 객체가 의존하게 된다.



결국 의존의 방향이 바뀐 것이다.




profile
베짱이는 개미가 밉다

0개의 댓글