Etc/2020

[Java] 객체 지향 프로그래밍(OOP, Object Oriented Programming)

메바동 2020. 6. 8. 11:51
728x90

객체 지향 프로그래밍 (Object Oriented Programming, OOP)

1. OOP (Object Oriented Programming)

객체 지향 프로그래밍이란 이전의 컴퓨터가 사고하는 대로 프로그래밍을 하는 컴퓨터 중심적 패러다임과는 다른 인간 중심적 패러다임 프로그래밍이라고 할 수 있다. 즉, 현실 세계의 사물들을 객체라 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍하는 것을 말한다.

1.1 클래스(Class)

연관되어 있는 변수와 메서드의 집합으로, 객체를 만들어 내기 위한 설계도 혹은 틀이라고 볼 수 있다.

1.2 객체(Object)

속성(Field, 변수)과 행위(Method)로 객체를 표현한다.
소프트웨어 세계에 구현할 대상이다. 클래스에 선언된 모양 그래도 생성된 실체로, 클래스의 인스턴스 라고도 부른다. 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖는다.

1.3 인스턴스(Instance)

클래스를 바탕으로 소프트웨어에 구현된 구체적인 실체로, 객체를 소프트웨어에 실체화하면 그것을 인스턴스라고 부른다.

  • 인스턴스는 객체에 포함된다고 볼 수 있다.
  • OOP 관점에서 객체가 메모리에 할당되어 실제 사용될 때 인스턴스 라고 부른다.

2. OOP (Object Oriented Programming)의 특성

2.1 상속성(Inheritance)

상속은 계층구조일 때 부모의 필드와 메서드를 물려받는 것이다. 상속을 통해 동일한 부분을 상속 받음으로써 개발 속도를 향상할 수 있다.

2.2 다형성(Polymorphism)

다형성은 메서드의 상속을 사용하는 개념으로 상위 클래스에서 정의된 내용을 하위에서 재정의 하는 오버라이딩 과 하나의 메서드를 여러 번 정의하는 오버로딩 이 있다.

2.3 정보은닉성(Information Hiding)

2.4 캡슐화(Encapsulation)

3. OOP (Object Oriented Programming)의 특징

3.1 추상화(Abstraction)

어떤 영역에서 필요로 하는 속성이나 행동을 추출하는 작업

  • 사물들의 공통된 특징, 즉 추상적 특징을 파악해 인식의 대상으로 삼는 행위를 말한다.
  • 구체적인 사물들의 공통적인 특징을 파악해서 이를 하나의 개념(집합)으로 다루는 수단을 말한다.

각 개체의 구체적인 개념에 의존하지 말고 추상적인 개념에 의존해야 설계를 유연하게 변경할 수 있다.

3.2 캡슐화(Encapsulation)

캡슐화는 낮은 결합도 를 유지할 수 있도록 해주는 객체 지향 설계 원리이다.

  • 높은 응집도와 낮은 결합도를 유지할 수 있도록 설계해야 요구사항을 변경할 때 유연하게 대처할 수 있다.

    1. 응집도(Cohesion)
      • 클래스나 모듈 안의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타낸다.
    2. 결합도(Coupling)
      • 어떤 기능을 실행하는 데 다른 클래스나 모듈들에 얼마나 의존적인지를 나타낸다.
  • 캡슐화는 정보 은닉 을 통해 높은 응집도와 낮은 결합도를 갖도록 한다.

    • 정보 은닉(information hiding)
      • 필요가 없는 정보는 외부에서 접근하지 못하도록 제한하는 것, private 키워드를 이용한다.
    • 정보 은닉이 필요한 이유
      • SW는 결합이 많을수록 문제가 많이 발생한다.
      • 한 클래스가 변경이 발생하면 변경된 클래스의 비밀에 의존하는 다른 클래스들도 변경해야 할 가능성이 커진다는 뜻이다.
  • 변하기 쉬운 것과 변하기 어려운 것

    • private
      • 변하기 쉬운 것은 감춘다.
      • 외부에서 변해도 영향을 받지 않는다.
      • Ex) 멤버 변수, 자료구조 등
    • public
      • 변하기 어려운 것은 드러낸다.
      • 변하기 어려우므로 외부에서 사용하는데 변경될 일이 적다.
      • Ex) 메서드들(Stack을 예로 들면 push, pop 등의 기능)

3.3 일반화 관계(Generalization)

일반화는 여러 개체들이 가진 공통된 특성을 부각해 하나의 개념이나 법칙으로 성립시키는 과정이다.

  • 일반화 관계는 객체 지향 프로그래밍 관점에서는 상속 관계 라 한다.
  • 따라서 속성이나 기능의 재사용만 강조해서 사용하는 경우가 많다.
  • 하지만 이는 일반화 관계를 극히 한정되게 바라보는 시각이다.

일반화 관계는 자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이다.

  • 일반화 관계는 한 클래스 안에 있는 속성 및 연산들의 캡슐화에 한정되지 않는다.
  • 즉, 외부 세계에 자식 클래스 자체를 캡슐화(또는 은닉)하는 것으로 확장된다.
  • 서브 클래스의 캡슐화는 외부 클라이언트가 개별적인 클래스들과 무관하게 프로그래밍을 할 수 있게 한다.

두 자식 클래스 사이에 "is a kind of" 관계(ISA)가 성립되지 않을 때 상속을 사용하면 불필요한 속성이나 연산도 물려받게 된다.

  • Stack을 구현할 때 ArrayList를 상속받게 되면 ArrayList의 isEmpty, size, add, remove 등의 메서드를 구현하지 않고 그대로 사용할 수 있다.
  • 그러나 ArrayList 클래스에 정의된 Stack과 관련 없는 수많은 연산이나 속성도 같이 상속받게 된다.
    • Stack "is a kind of" ArrayList 관계가 아니기 때문에 일부 기능만을 사용하기 위해 부모로 만들지 않는다.
    • ArrayList 클래스의 set과 같은 연산도 같이 상속되어 Stack의 무결성 조건인 LIFO(Last In First Out)에 위배된다.

어떤 클래스의 일부 기능만 재사용하고 싶은 경우에는 위임(delegation) 을 사용한다.

  • 자신이 직접 기능을 실행하지 않고 다른 클래스의 객체가 기능을 실행하도록 위임하는 것
  • 따라서 일반화 관계는 클래스 사이의 관계지만 위임은 객체 사이의 관계다.
  • 즉, 기능을 재사용할 때는 위임을 이용한다.

※ 상속 : IS-A, 위임 : HAS-A

3.4 다형성(Polymorphism)

다형성은 서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력이다.

  • 다형성이 상속과 연계되어 동작하면 매우 강력한 힘을 발휘한다.
  • 다형성과 일반화 관계는 코드를 간결하게 할 뿐 아니라 변화에도 유연하게 대처할 수 있게 한다.
  • 다형성을 사용하는 경우에는 구체적으로 현재 어떤 클래스 객체가 참조되는지와 무관하게 프로그래밍을 할 수 있다.
  • 일반화 관계에 있을 때 부모 클래스의 참조 변수가 자식 클래스의 객체를 참조할 수 있기 때문에 새로운 자식 클래스가 추가되더라도 코드는 영향을 받지 않는다.
  • 단, 부모 클래스의 참조 변수가 접근할 수 있는 것은 부모 클래스가 물려준 변수와 메서드 뿐이다.

피터 코드의 상속 규칙(Peter Coad)

상속의 오용을 막기 위해 상속의 사용을 엄격하게 제한하는 규칙들

  • 5가지 규칙 중 어느 하나라도 만족하지 않는다면 상속을 사용해서는 안된다.
    1. 자식 클래스와 부모 클래스 사이는 '역할 수행'관계가 아니어야 한다.
    2. 한 클래스의 인스턴스는 다른 자식 클래스의 객체로 변환할 필요가 절대 없어야 한다.
    3. 자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다.
    4. 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다.
    5. 자식 클래스가 '역할', '트랜잭션', '디바이스' 등을 특수화해야 한다.

4. OOP (Object Oriented Programming) 설계 원칙

4.1 단일 책임 원칙(SRP, Single Responsibility Principle)

객체는 단 하나의 책임만 가져야 한다.

  • 책임이란?
    • 해야 하는 것, 할 수 있는 것, 해야 하는 것을 잘 할 수 있는 것
    • 객체에 책임을 할당할 때는 어떤 객체보다도 작업을 잘 할 수 잇는 객체에 책임을 할당해야 한다.
  • 설계 원칙을 학습하는 이유?
    • 예측하지 못한 변경사항이 발생하더라도 유연하고 확장성이 있도록 시스템 구조를 설계하기 위해서
  • 좋은 설계란?
    • 기본적으로 시스템에 새로운 요구사항이나 변경이 있을 때 가능한 한 영향 받는 부분을 줄이도록 하는 것
    • 즉, 객체가 변해야하는 이유는 단 1개

책임 분리

  • 한 클래스에 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 잇는 것을 하나로 만들어야 한다.
    • 책임을 많이 질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.

산탄총 수술

  • 하나의 책임이 여러 개의 클래스들로 분산되어 있는 경우에도 단일 책임 원칙에 입각해 설계를 변경해야 하는 경우도 있다.
    • 예를 들어 로깅, 보안, 트랜잭션과 같은 횡단 관심으로 분류할 수 있는 기능
    • 횡단 관심에 속하는 기능은 대부분 시스템 핵심 기능(하나의 책임) 안에 포함되는 부가 기능(여러 개의 클래스로 분리)이다.
  • 부가 기능을 별개의 클래스로 분리해 책임을 담당하게 한다.
    • 즉, 여러 곳에 흩어진 공통 책임을 한 곳에 모으면서 응집도를 높인다.
    • 그러나 이런 독립 클래스를 구현하더라도 구현된 기능들을 호출하고 사용하는 코드는 해당 기능을 사용하는 코드 어딘가에 포함될 수 밖에 없다.

관심지향 프로그래밍(AOP)과 횡단 관심 문제

  • 횡단 관심 문제(Cross Cutting Conern)를 해결하는 방법?
    • 관심지향 프로그래밍 기법(AOP, Aspect Oriented Programming)
  • AOP란?
    • 횡단 관심을 수행하는 코드는 aspect라는 특별한 객체로 모듈화하고 weaving이라는 작업을 통해 모듈화한 코드를 핵심 기능에 끼워넣을 수 있다.
    • 이를 통해 기존의 코드를 전혀 변경하지 않고도 시스템 핵심 기능에서 필요한 부가 기능을 효과적으로 이용할 수 있다.
    • 만약 횡단 관심에 변경이 생긴다면 해당 aspect만 수정하면 된다.
  • AOP 관련 용어
    • JoinPoint: 애플리케이션 실행 중의 특정한 지점
    • Advice: 특정 JoinPoint에 실행하는 코드
    • PointCut: 여러 JoinPoint의 집합체로, 언제 Advice를 실행할지 정의할 때 사용
    • Aspect: 애플리케이션이 가져야 할 로직과 그것을 실행해야 하는 지점을 정의한 것
    • Weaving: 애플리케이션 코드의 해당 지점에 Aspect를 실제로 주입하는 과정

4.2 개방-폐쇄의 원칙(OCP, Open Closed Principle)

기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.

  • 설계의 기본(의존 역전 원칙(DIP)과 밀접)이라고 할 수 있다.
  • 인터페이스(변하지 않는 것)에서 구체적인 출력 매체(변하는 것)를 캡슐화해 처리하도록 해야 한다.
  • OCP에 위반하지 않는 설계를 할 때 가장 중요한 것은 무엇이 변하는 것인지, 무엇이 변하지 않는 것인지를 구분해야 한다는 점이다.
    • 변해야 하는 것은 쉽게 변할 수 있게 하고, 변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 한다.
  • 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다.
    • 특히 단위 테스트를 수행할 때 매우 중요하다.

4.3 리스코프 치환 원칙(LSP, Liskov Substitution Principle)

일반화 관계에 대한 이야기며, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다.

  • LSP는 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미이다.
    • 즉, LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다.
  • 일반화 관계
    • "is a kind of" 의 관계
    • 예를 들어, 원숭이 is a kind of 포유류 -> 포유류(부모 클래스), 원숭이(자식 클래스)
  • LSP를 만족시키는 간단한 방법은 재정의하지 않는 것이다.
    • 즉, 부모 클래스에서 상속받는 메서드들이 자식 클래스에 오버라이드, 즉 재정의되지 않도록 하면 된다.
    • OOP의 특징에서 언급했던 "피터 코드의 상속 규칙"의 '자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행한다.'라는 규칙을 지키는 것은 LSP를 만족시키는 하나의 방법에 해당한다.

4.4 인터페이스 분리 원칙(ISP, Interface Segregation Principle)

인터페이스를 클라이언트에 특화되도록 분리시키라는 원칙이다.

  • 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다.
  • 단일 책인 원칙(SRP)과 밀접하다.
  • SRP와 ISP 사이의 관계
    • 어떤 클래스가 여러 책임을 수행하게 되면 방대한 메서드를 가진 비대한 클래스가 될 가능성이 커진다.
      • 이렇게 비대한 클래스를 단일 책음을 갖는 여러 클래스로 분할하면 SRP를 만족한다.
      • 또한 각자의 인터페이스를 제공한다면 ISP를 만족할 수 있다.

4.5 의존 역전 원칙(DIP, Dependency Inversion Principle)

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙이다.

  • 객체 사이에 서로 도움을 주고 받으면 의존 관계가 발생하는데, DIP는 이때의 가이드라인에 해당한다.
    • 클래스 간 의존 관계란?
      • 한 클래스가 어떤 기능을 수행하려고 할 때, 다른 클래스의 서비스가 필요한 경우
  • OCP가 되려면 기본적으로 DIP가 만족되어야 한다.
  • 변하기 쉬운 것과 변하기 어려운 것을 구분하는 방법?
    • 변하기 어려운 것, 거의 변화가 없는 것: "정책", "전략"
    • 변하기 쉬운 것: "구체적인 방식", "사물"
  • 객체지향 관점에서 변하기 어려운 추상적인 것들을 표현하는 수단
    • 추상 클래스
    • 인터페이스
  • DIP를 만족시키는 방법
    • 어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 인터페이스나 추상 클래스와의 의존 관계를 맺도록 설계해야 한다.
  • DIP를 만족하는 설계는 변화에 유연한 시스템이 된다.
    • 의존성 주입(DI, Dependency Injection) 이라는 기술로 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다.
    • 의존성 주입이란?
      • 클래스 외부에서 의존되는 것을 대상 객체의 인스턴스 변수에 주입하는 기술




Reference

728x90