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

객체지향 프로그래밍1

JunsuKim 2022. 10. 4.
728x90

객체 간 관계 vs 클래스 간 관계

하나의 프로그램을 개발할 때 여러 종류의 객체를 사용한다. 

객체는 보통 독립적으로 동작하지 않으며, 다른 객체와 다양한 관계를 맺는다.

 

객체 간 관계는 보통 동적 관계이기에 프로그램이 실행되는 동안 바뀔 수 있다.

객체 간 관계는 크게 두 개의 관계로 구분된다.

  • 사용 관계(use-a) -> 논리적 관계
  • 포함 관계(has-a) -> 물리적 관계(구현 형태에 의해 결정된다.)
    • 한 객체가 다른 객체를 멤버 변수로 유지하는 경우
    • 논리적으로 전체-부분 관계를 나타내기 위해 주로 사용한다.

이번에는 클래스 간 관계를 보자.

클래스 간 관계는 객체 간 관계와는 달리 정적이다. 따라서 코드를 수정하지 않는 이상 관계가 변하지 않고 고정된다.

클래스 간 관계 또한 두 가지의 종류가 있다.

  • Subclassing: 한 클래스의 상태와 행위의 구현을 재사용 -> 상속(is-a)
  • Subtyping: 외부 모습만 재사용한다. -> 구체화

A와 B라는 클래스가 있을 때 B가 A의 subtype이면 A 타입을 기대하는 어느 곳이나 B 클래스 객체를 안전하게 사용할 수 있다.

즉, 계약 기반 프로그래밍이 가능하다.

※ SubClassing을 하면 Subtyping의 효과를 얻을 수 있다.

의존 관계

한 클래스를 구현할 때 다른 클래스를 사용하면 두 클래스 간 혹은 두 클래스 객체 간 관계가 있다는 것을 의미한다.

관계의 종류와 무관하게 관계의 수 측면에서 접근할 때에 이들 관계를 간단히 의존 관계라 한다.

public class A extends B implements C, D {
    private E e;
    public void foo(F f) {}
    public void bar() {
        G g;
    }
}

위 코드에서 A는 B, C, D, E, F, G에 의존한다.

  • 한 클래스가 의존하는 클래스의 수는 적을수록 좋다.
  • 의존하는 클래스가 구체적 클래스(상속 관계에서 단말)이면 의존 관계를 바꾸기 위해 코드 수정이 필요하다.
    따라서 구체적 클래스 대신 추상 타입(상속 관계에서 상위 클래스 혹은 interface)에 의존해야 한다.

사용 관계(use-a)

가장 일반적인 관계이며 논리적 관계이다.

멤버 변수가 아닌 다른 클래스의 객체를 메소드의 인자로 받아 사용하거나 메소드 내에서 다른 클래스의 객체를 생성하여 사용하면 무조건 사용 관계이다.

// 사용 관계 예시
public class Child {
    public void wash(Towel towel) { 
        ....
    }
}

public class Camera {
    public Picture takePicture() {
        Picture picture = nwe Picture();
        ....
        return picture;
    }
}
  • Child 객체는 Towel 객체를 사용한다.
  • Camera 객체는 Picture 객체를 사용한다.

포함 관계(has-a)

객체가 다른 객체를 멤버로 유지하는 경우를 의미한다.

사용 관계에서 여러 메소드가 동일 객체를 사용할 경우 멤버로 유지 가능하다.

포함 관계는 크게 부분전체 관계부분전체 관계가 아닌 경우로 구분된다.

  • 부분전체 관계: 부분전체 관계는 집합(aggregation)과 복합(composition) 관계로 구분된다.
    • 집합 관계: 전체가 제거되어도 부분은 남는다.
      ex) 우물에 오리가 떠있다고 생각해보자. 이때 우물이 갑자기 사라진다해서 오리가 사라지진 않는다.
    • 복합 관계: 전체가 제거되면 부분도 제거된다.
      ex) 아파트가 무너져 내리면 각 방들 또한 무너져 내린다.
  • 부분전체 관계가 아닌 경우: 연관(association) 관계라 한다.
// 집합 관계
public class Computer {
    private CPU cpu;
    ...
    public Computer(String cpuType, ...) {
        this.cpu = CPU.getInstance(cpuType);
    }
}

// 복합 관계
public class Computer {
    private CPU cpu;
    ...
    public Computer(CPU cpu, ...) {
        this.cpu = cpu;
    }
}

포함 관계는 다수성을 파악할 필요가 있다. 이에 따라 구현 방법이 달라지기 때문이다.

연관 클래스

두 클래스 간 연관 관계가 복잡할 경우(다대다 관계) 연관 클래스를 활용할 수 있다.

예를 들어 학생과 수강 교과가 있다고 하자.

한 학생은 여러 교과에 수강할 수 있고, 한 교과에는 여러 학생이 참여할 수 있다.

즉 다음과 같은 관계가 이루어진다.

이를 연관 클래스로 구현하면 다음과 같이 된다.

포함 관계를 이용한 모듈화

긴 함수 혹은 긴 클래스는 응집성이 떨어지기 때문에 보통 여러 개의 작은 함수로 모듈화한다.

모듈화를 하는 방법은 다음과 같다.

  1. 멤버 변수를 모아 하나의 개념을 표현할 수 있으면, 멤버 변수와 이와 관련된 메소드를 묶어 새 클래스를 정의하고 이 클래스의 객체를 기존 클래스의 멤버 변수로 추가한다.
  2. 기존 클래스의 메소드 중 일부는 이 클래스의 메소드를 호출하는 단순 중계(pass through) 메소드로 바꾼다.
public class A {
    private ?c;
    private ?d;
    ....
    
    public void foo(args) {
        ....
    }
    private void bar() {
        ....
    }
    ....
}

위의 코드를 모듈화하면 다음과 같이 된다.

public class B {
    private ?c;
    private ?d;
    
    public void foo(args) {
        ....
    }
    private void bar() {
        ....
    }
}


public class A {
    private B b;
    ....
    
    public void foo(args) {
        b.foo(args);
    }
    ....
}

상속

객체지향에서는 한 클래스를 이용하여 해당 클래스를 특수화한 새 클래스를 정의할 수 있다.

만약 개구리 클래스가 있다면, 이를 특수화한 황소개구리 클래스를 정의할 수 있다.

이때, 개구리가 가져야 하는 공통 특성(상태, 행위)은 개구리에 정의하고, 황소개구리는 개구리와 구별되는 요소만 구현하면 된다.

개구리를 황소개구리 클래스의 부모 클래스라 하며, 황소개구리는 개구리의 자식 클래스라 한다.

 

보통 부모 클래스를 정의한 후 자식 클래스를 정의하지만, 반대로 여러 클래스의 공통된 부분을 뽑아 일반화한 클래스를 정의할 수도 있다.

상속은 코드 중복을 줄여주고 코드를 재사용할 수 있도록 도와준다는 장점이 있다.

하지만 코드 재사용 목적만으로 상속하는 것은 바람직하지 않다.

논리적으로 타당할 경우 즉 (is-a)에만 상속해야 한다. 하지만, 논리적으로 타당하더라도 객체지향에서는 적합하지 않을 수 있다.

public abstract class Pet {
    private String name;
    public String getName() {return name; }
    public void setName(String name) { this.name = name; }
    public abstract void makeSound();
}

public class Cat extends Pet {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

public class Dog extends Pet {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

다형성

  • 참조 타입의 변수가 가리키는 실제 객체의 클래스에 따라 그것에 맞는 메소드가 호출된다는 것을 의미한다.
  • 변수 타입이 subtyping되어 여러 다른 종류의 객체를 가리킬 수 있어야 한다.
  • 실행 시간에 호출할 함수를 결정해야 한다.(이른 바인딩 vs 늦은 바인딩)
  • 보통 사용하는 변수의 타입에 의해 호출할 수 있는 것이 결정되지만, 늦은 바인딩을 하면 실제 호출되는 것은 그 변수가 참조하는 실제 객체의 클래스에 의해 결정된다.
  • 자바는 모든 메소드가 가상함수이며, 오직 참조 타입의 변수만 사용하기 때문에자동으로 늦은 바인딩을 제공한다.

상속의 문제점

  • 상속은 클래스 간 정적 관계이므로 코드를 수정하지 않는 이상 관계가 계속 유지된다.
  • 자식 클래스는 자신의 필요와 무관하게 부모 클래스의 모든 메소드를 상속받는다.
  • 자식 클래스는 자신이 상속받은 모든 메소드에 대해 다음 중 하나를 해야 한다.
    1. 그대로 사용
    2. 재정의
      • 추상 메소드
      • 일반 메소드
    3. 사용할 수 없도록 조치
      • 보통 빈 메소드로 재정의한다.
  • 일반 메소드 혹은 빈 메소드로 재정의한다면 상속 자체가 적절하지 않을 수 있으므로 재설계가 필요할 수 있다.
  • 여러 자식 클래스가 특정 메소드가 필요없으면 각 클래스에 해당 메소드를 빈 메소드로 재정의하기 보다 해당 메소드를 빈 메소드로 재정의한 중간 클래스를 정의하여 사용하는 것이 바람직할 수 있다.

구체화

상속은 논리적으로 관련있는 것들을 묶어주는 역할을 했다면, 구체화는 논리적으로 관련이 없지만 같은 이름의 메소드를 가지는 것들을 묶어주는 역할을 한다.

자바에서는 interface를 통해 구체화 관계를 제공한다.

public interface Flyable {
    void fly();
}

public class Bee implements Flyable {
    @Override
    public void fly() {}
}

public class Airplane implements Flyable {
    @Override
    public void fly() {}
}

객체지향과 코드

  • 보통 기존 구조화 프로그래밍과 비교하여 함수의 인자 수가 하나 줄어든다.
  • 구조화에서는 함수에 처리할 데이터를 전달해야 한다.
  • 다형성을 이용하면 조건문의 사용이 없어진다.

객체지향과 관련 오해

  1. 접근 제어 때문에 불필요한 getter와 setter가 많다.
    -> 모든 멤버 변수에 대한 getter와 setter가 필요한 것이 아닌, 필요한 멤버 변수에 대해서만 생성해주면 된다.
  2. 객체지향으로 개발하면 코드가 길고 복잡하다.
    -> 처음 백지 상태에서 프로그램을 개발할 경우 구조화 방법이 빠르고 코드의 길이도 짧으나, 확장성 등을 고려하여 올바르게 설계하여 구현하면 객체지향 코드가 훨씬 유지보수하기가 쉽고 유연하다.
  3. 객체지향으로 구현하면 무조건 유연하다.
    -> 올바르게 설계되지 않은 객체지향 코드는 다른 방식과 마찬가지로 유지보수하기가 힘들다. 특히, 상속을 고려하지 않은 클래스를 상속하여 유연한 코드를 만드는 것이 매우 힘들다.
728x90

'디자인 패턴 > 객체지향 프로그래밍' 카테고리의 다른 글

객체지향 프로그래밍2  (0) 2022.10.04

댓글