OOP

객체지향의 근본 조건

Posted by yunki kim on March 6, 2021

1. 상속(Inheritance) 

  대부분의 객체지향을 공부한 사람들은 상속을 하면 코드를 재활용할 수 있다는 것을 알고있다. 하지만 단순하게 상속을 코드를 재활용하는 데에만 사용한다면 좋은 코드를 작성할 수없다. 

  객체는 속성과 메서드를 가진다. 따라서 객체지향에서 상속은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받은 것이다. 아리스토텔레스는 기존의 분류 계층에서 새로운 하위 계층이 생성될 경우, 이 하위계층의 특징은 기존 상위 계층의 특징을 기본적으로 소유함과 동시에 자신만의 특징을 추가할 수 있다고 정의했다. 따라서 어떤 분류에 속하는 다른 객체가 어떤 특징을 가지고 있다면 그 분류와 하위 분류에 속하는 다른 객체도 그 속성을 가지고 있을거란 추론이 가능하다. 

  상위에 위치한 분류 계층을 하위 분류 계층의 일반화라고 한다. 하위에 위치한 분류를 상위분류 계층의 특수화 라고 한다. 상속을 받으면 자식 클래스는 물려받은 속성과 메소드가 코드에 명시되지 않지만, 상속받은 속성과 메서드를 거의 아무 제약 없이 사용할 수 있다.

  자바의 경우 public > protected > default(명시하지 않음) > private 순으로 접근 가능한 범위가 정해진다

    public: 패키지, 상속 유무에 관련없이 모든곳에서 접근할 수 있다.

    protected: 상속받은 모든 곳(다른 패키지 포함)에서 접근 가능

    default(명시하지 않음): 같은 패키지에서만 접근 가능

    private: 상속받은 클래스도 사용 못함

  클래스의 관계 측면에서 상속의 역할은 자식 클래스와 부모 클래스를 강하게 연결하는 것이다. 즉, 자식클래스는 부모클래스의 특징을 강하게 따라야 한다. 이를 통해 다음과 같은 이점을 얻을 수 있다

    1. 코드 관점

      1.1코드의 재사용 가능

        자식 클래스는 부모 클래스의 속성과 기능을 중복 코딩할 필요 없이 그대로 사용한다. 

      1.2 코드의 수정, 확장이 편리해 진다

        클래스 그룹에서 부모 클래스의 코드만 수정하면 상속받은 자식 클래스는 자동으로 같이 수정된다. 또한 새로운 코드를 추가하고 싶을        때 부모 클래스의 코드만 추가하면 되기 때문에 수정과 확장에 용이하다.

    2. 개발의 편의성 관점

      2.1 분류하고 범주 정하기에 익숙한 사람의 사고와 비슷한 프로그래밍을 할 수 있게 해준다. 

      2.2 인간에게 익숙한 분류와 범주의 개념 적용

        코드를 상속 관계로 묶어 클래스 간의 체계화된 전체 구조를 파앆할 수 있다. 이는 코드를 쉽게 이해해 개발 편의성과 생산성을 높여줄        수 있는 객체지향의 중대한 장점이다.

    3. 낮은 관계의 의존성과 높은 기능의 집중도를 위한 관점

      3.1 오버라이딩 가능

        부모 클래스의 기능을 자신의 특성에 맞게 오버라이드할 수 있다. 같은 클래스 그룹에서 해당 클래스의 고유한 특징에 맞는 메서드 안           의 로직을 달리 구현할 수 있다.

      3.2 폴리모피즘(다형성) 가능

        상속과 오버라이드를 연동해 다형성이라는 객체지향 요소를 구현할 수 있다. 

2. 오버로딩(Overloading)

  메서드의 이름은 같지만 메서드의 파라미터를 다르게 해서 같은 메서드 이름을 가졌어도 별개의 메서드로 보고 접근을 하는 것이다. 흔히 이를 명칭이 비슷하다는 이유로 오버라이딩과 혼동하는데 오버라이딩은 메서드, 매개변수와 타입이 동일해야 한다. 

3. 오버라이딩(Overriding)

  상속을 받은 부모 클래스에서 정의한 메서드를 자식 클래스의 특징에 맞게 로직을 덮어쓰기 해서 재구현하는 개념이다.

  다음과 같은 클래스 그룹이 있다고 해보자

  1번은 부모클래스를 기준으로 JetAirPlane을 생성했고 2번은 본인 클래스르 기준으로 생성을 했다. 비록 두 인스턴스의 결과값은 같겠지만 객체지향이 추구하는 낮은 관계의 의존성과 높은 기능의 집중도 관점에서는 1번 방식이 더 적합하다. 이에 대한 자세한 설명은 폴리모피즘에 대한 설명에서 하겠다.

4. 폴리모피즘(Polymorphism)

  정의:

    같은 그룹에 속하는 클래스들의 동일한 메소드를 호출할 때 자식 클래스들이 저마다 다른 로직을 수행하고 리턴하는 것.

  폴리모피즘은 이해 해야만 클래스 교체와 확장을 자유롭게 할 수 있다. 폴리모피즘은 낮은 관계의 의존성과 높은 기능의 집중도를 구현할 수 있는 핵시 기법이다. 폴리모피즘은 같은 그룹에 속하는 클래스들의 동일한 메서드를 호출할 때 자식 클래스들이 각자의 다른 로직을 수행하고 리턴한다. 다음과 같이 폴리모피즘을 구현하는 환경을 만들면 된다

  다음과 같은 클래스 그룹이 있다고 하자

결과는 다음과 같이 출력된다

main 메서드에서 5, 6번째 줄의 실행 과정은 다음과 같다

  Pilot 생성자에서 DefaultAirplane인터페이스의 DefaultAirplane로직으로 생성된 defaultAirplaneRecieved 객체를 인자로 받아 defaultAirplane1 전역변수에 저장한다. intoAirplane메서드에서는 Default 인터페이스의 DefaultAirplane 로직으로 저장된 defaultAirplane객체의 airplaneFeature 메서드를 실행한다. 여기서 'DefaultAirplane 인터페이스의 DefaultAirplane로직'을 이해하는 것이 폴리모피즘을 이해하는데의 핵심이다. 

main함수 5번째 줄:(defaultAirplane 변수의 인터페이스인 에서의 인터페이스는 자바에서의 인터페이스와는 다른,임의로 정한 의미이다.)

  defaultAirplane 변수의 인터페이스인 DefaultAirplane의 실제 로직 구현은 DefaultAirplane클래스 에게 맡기겠다는 의미이다.  

 

폴리모피즘을 사용한 구성의 장점

  1. 관계 의존성이 낮아진다

    위의 예제에서 JetAirPlane, StealthAirplane은 모두 DefaultAirplane에만 의존한다. 

  2. 클래스의 코드 변경 없이, 기능의 수정과 확장이 무제한으로 가능하다. 

    위의 코드를 보면 jetAirPlane, StealthAirplane은 그들의 부모 클래스인 DefaultAirplane인터페이스에서 실행된다. 따라서 기능을 부모    클래스의 변경 없이 수정할 수 있다.

 

폴리모피즘을 위한 3가지 원칙

  효과적인 폴리모피즘 활용을 위해서는 다음 3가지 조건이 필요하다.

  1. '기능을 제공할'  비슷한 종류의 클래스들을 모아 클래스들을 모아 클래스 그룹으로 조직화한다.

    위 예제에서 비행기 클래스 그룹은 파일런 클래스에게 기능을 제공하는 클래스이다. 비행기 클래스들은 intoAirplane이라는 메서드 인터페이스를 일관되게 통일해서 DefaultAirplane을 부모 클래스로 하는 클래스 그룹으로 조직화 했다.

  2. 기능을 제공할 클래스 그룹의 인터페이스를 통일한다.

    부모 클래스를 기준으로 자식 클래스도 상태, 동작을 통일해야 한다. 클라이언트 클래스에서는 폴리모피즘을 위해서 반드시 부모클래스 인터페이스의 자식 클래스 로직으로 선언하고, 자식클래스 인터페이스의 자식 클래스 로직으로 선언하지 않아야 한다.

  3. 클라이언트 클래스에서는 쓰고싶은 클래스 그룹의 부모 클래스만 의존하고 자식 클래스를 인자로 넘겨받아 저장한다.

    클라이언트 클래스는 폴리모피즘을 위해 부모 클래스 인터페이스의 자식 클래스 로직으로 선언한다. 자식 클래스 인터페이스의 자식 클래 스 로직으로 선언하지 않는다.

 

5. 캡슐화(Encapsulation)

  정의: 객체지향에서는 속성을 숨기고 보호하는 캡슐화, 기능을 분리하여 어떤 틀 안에 숨기고 보호하는 역할을 수행하며 변하는 기능을 분리해 어떤 틀 안에 숨기호 보호한다. 

  정보은닉: 집합과 분해로 불필요한 정보를 감추고 필요한 정보만 추상화하여 추출해서 보여줄 수 있다.

  1. 속성의 캡슐화

    캡슐화를 위해서 통상적으로 속성을 private으로 선언하고 getter/setter를 통해 조작한다. 이때 이와같이 하는 이유는 캡슐화를 통해 객    체 안의 데이터를 다를 객체가 잘못 조작하는 것을 막기 위해서이다. 캡슐화를 잘 했다면 다른 객체가속성에 직접 접근할 수 없다. 따라서      속성이 보호되는 효과가 있다. 그래서 속성의 주인 객체가 따로 의도를 가지고 조작한 값이 그대로 보존되는 효과가 있다. 

  2. 기능의 캡슐화

    객체지향에서 캡슐화의 의미는 객체 간의 관계에서도 사용된다. 즉, '변하는 기능을 분리해서 따로 캡슐화'를 한다 라는 객체지향의 원칙이다. 캡슐화는 관련있는 여러 정보들을 어떤 틀 안에 담는 것이므로 객체의 지능 중 일부를 그 객체로부터 분리해서 다른 클래스 그룹으로 새롭게 구성하는것 또한 캡슐화이다. 

  객체의 일부 기능을 다른 클래스 그룹의 새로운 틀로 담았기 때문에 이렇게 객체의 기능을 별도의 클래스 그룹으로 분리해 담으면, 프로그램의 유연성과 확장성이 좋아진다. 하나의 클래스에 기능을 모두 담아버리면 그 기능에 대한 확장이 어려운 반면 객체의 기능을 별도의 클래스 그룹으로 분리하면 그 기능을 더 확장할 수 있다.

  3. 정보의 캡슐화(정보은닉), 집합과 분해

    캡슐화를 지키면 정보은닉 규칙도 함께 지킬 수 있다. 정보은닉을 잘 지원하는 객체지향 요소가 바로 인터페이스이다. 개발자는 인터페이스의 기능 명세를 설계하고 기능 명세의 구현은 하위 클래스에 위임하다. 클라이언트 클래스는 인터페이스에만 의존되 있다. 이렇게 하면 클라이언트 클래스는 인터페이스에 명시된 기능의 용도만 알기 때문에 정보의 캡슐화를 지킬 수 있다. 

  인터페이스를 만들기 위해서는 단순화를 해야하는데 이때는 불필요항 세부 요소들을 배제하고 알아야 한는 큰 그림에서 대상을 다루는 추상화를 했다. 이 추상황의 과정이 집합이고 반대로 전체를 세부로 분할하는 과정이 분해이다. 처음에는 대상을 제대로 알기 위해 집합을 했지만 나중에는 다시 분해할 수 있다. 객체지향 소프트웨어에서는 '패키지' 개념으로 표현된다. 패키지는 서로 관련성이 높은 클래스를 어떤 기준을 가지고 통합한다. 

 

캡슐화를 제대로 알고 있다면 다음과 같은 조건을 알아야 한다.

  1. 되도록 속성을 private으로 만들고 getter/setter 메서드로 호출한다

  2. 변하는 기능을 인식할 수 있고, 변하지 않는 기능으로부터 분리하여 별도의 클래스 그룹으로 추출할 수 있다. 따라서 기능 수정과 확장 요구에 유연하게 대처할 수 있다.

  3. 소프트웨어에서 얼만큼의 정보를 보여주고, 감춰야 할지 알고있다. 정보의 집합과 분해에 능숙하다. 정보의 추상화 개념을 알고 쓸 줄 안다. 따라서 제공하는 인터페이스가 깔끔해지고, 코드의 가독성이 높아져 개발 편의성이 높아진다. 또한 다른 클라이언트가 필요이상의 정보가 담긴 클래스를 의존하고 조작하는 상황을 막을 수 있다.

 

6. 인터페이스(Interface)

  인터페이스는 어떤 대상의 의미를 구현할 때 지켜야 할 규칙과 표준을 의미한다 또한 상호 교류하는 대상끼리 정보를 주고 받는 방법에 대한 규칙이자 표준이다. 이것은 구체적인 사물 또는 추상적인 개념의 의미를 완벽하게 구현하기 위해서는 제시하는 인터페이스를 지켜야 한다는 의미이다.

  인터페이스의 효과

    1. 다중 상속 효과가 가능하다

      Java는 extends를 통해 하나의 부모 클래스로부터 상속받을 수 있다. 하지만 추가로 인터페이스를 implements해서 마치 다중 상속을       하는 효과를 얻을 수 있다. (ex. public class A extends B implements C)

    2. 인터페이스와 로직이 명확하게 분리된다

      클라이언트 클래스는 인터페이스의 메서드 정의만 보고 메서드 구현은 자식 클래스에서 했을 것이라 가정한다. 실제 메서드의 구현은         인 터페이스를 상속받은 자식 클래스에서 담당한다. 

    3. 인터페이스와 로직이 명확히 분리된다. 그래서 부모 클래스 코드가 깔끔해 진다

    4. 인터페이스와 로직이 명확하게 분리되어 보다 중요한 메서드 명세를 더 강조할 수 있다. 

       인터페이스는 다른 요소는 제거하고 메서드 명세만 도드라지게 노출한다. 이로 인해 인터페이스의 기능을 쓰려는 클라이언트 클래스가        인터페이스에 정의된 메서드 목록만 알고 구체적인 로직은 어떻게 구현되었는지 자세히 알 필요가 없다는 것을 자연스럽게 유도하고 강        조하는 효과가 있다. 복잡한 로직이 함께 포함된 클래스보다 좀 더 직관적으로 메서드 목록을 클라이언트 클래스에게 알려주는 효과가          있다.

    5. 인터페이스와 로직이 명확히 분리되어 외부에 노출할 필요 없는 로직을 캡슐화 한다. 

        인터페이그는 필요없는 로직을 숨기는 점에서 '캡슐화' 효과도 있다. 클라이언트 클래스로부터 구현 클래스의 로직을 숨길 때 얻는 효           과는 크다. 다른 클라이언트가 필요 이상으로 구현 클래스를 의존하고 조작하는 것을 막을 수 있다. 그래서 좋은 소프트웨어의 기준인           낮은 관계의 의존성과 높은 기능의 집중도를 지향할 수 있다.

7. 위임(Delegation)

  상속의 단점:

      상속의 무분별한 사용은 몇가지 문제점을 야기한다. 

        3개의 클래스가 하나의 부모 클래스를 상속받고있다고 하자. 이때 3개의 자식 클래스 중 1개의 클래스는 필요한 기능이지만 나머지 클래스에게는 필요 없는 기능을 부모 클래스에 추가한다고 해보자. 그러면 2개의 자식 클래스는 필요 없는 기능을 강제로 상속받게 된다. 이 문제를 해결하기 위해 그 2개의 클래스에 대해 아무 작업도 하지 않게 오버라이드를 했지만 이는 유지보수 측면에서 문제가 된다. 이러한 방식은 코드의 중복을 최소화 해야 한다는  원칙에 어긋난다. 상속은 코드의 중복을 줄이기 위해 사용하는 방식인데 오히려 코드의 중복이 생겨버린다. 또 한 해당 코드에 수정이 가해질때 부모 클래스의 메서드를 수정하고 자식클래스 중 오버라이드 된 클래스에 대해 주정을 일일히 가해야 한다. 

        물론 오버라이드 하지 않고 무시하는 방식도 존재하지만 이렇게 하면 필요 없는 기능이 상속되기에 소프트웨어 무결성 관점에서 원칙에 어긋나게 된다. 

         상속의 장점은 부모 클래스의 메서드를 재사용하면서 부모 클래스의 오버라이드 기능을 이용해 폴리모피즘 효과를 유도한다는 점에 있다. 하지만 상속은 모든 자식 클래스들이 부모 클래스의 속성과 기능을 무조건 모두 상속받아야 한다. 한번 상속받은 기능은 상속 관계를 끊지 않는 한 상속 받는 것으로 고정되 변경이 불가능 하다.

         위임은 객체의 큭정 기능을 수행하기 위해 다른 클래스 그룹을 생성하고 이 클래스 그룹의 기능을 사용하기 위해 클라리언트 클래스가 연결을 맺은 구조다. 이렇게 객체 간의 관계를 위임 구조로 조합하면 상속으로 강하게 결합된 자식 클래스를 고치는 대힌 새로운 클래스 그룹을 만들거나 이미 만들어진 클래스그룹의 자식 클래스를 추가하여 기존 코드의 수정은 최소화하면서 기능의 수정, 확장을 자유롭게 적용할 수 있다.

출처 - 한번 읽으면 두번 깨닫는 객체지향 프로그래밍