Skip to content

Latest commit

 

History

History
370 lines (209 loc) · 15.3 KB

File metadata and controls

370 lines (209 loc) · 15.3 KB

3. 그 밖의 객체지향 개념들

3.1. 생성자

생성자는 구조적 프로그래머에게 새로운 개념일 수 있다.

객체지향적이지 않은 언엉에서 일반적으로 생성자가 되지 않지만 C/C++같은 언어의 구조체는 생성자가 포함된다.

자바와 C#과 같은 일부 객체지향언어에서 생성자란 클래스의 이름과 같은 이름을 지닌 메서드를 말한다.

객체지향 언어에서 생성자는 반환값이 없다.

3.1.1. 생성자는 언제 호출되는가

새로운 객체가 생성될 때 가장 먼저 일어나는 일 중 하나는 생성자 호출이다.

Cabbie myCabbie = new Cabbie();

new 키워드는 택시 기사 클래스의 새 인스턴스를 만들고 이에 필요한 메모리를 할당한다.

그런 다음 생성자가 호출되어 매개변수 목록의 인수를 전달한다.

따라서 new Cabbie()코드는 Cabbie 객체(정확하게 말하면 Cabbie 클래스)를 인스턴스화하고 생성자인 Cabbie메서드를 호출한다.

3.1.2. 생성자의 내부는 어떨까?

아마도 생성자의 가장 중요한 기능은 new키워드가 발견될 때 할당된 메모리를 초기화하는 일일 것이다.

즉, 생성자에 포함된 코드는 새로 생성된 객체를 초기의 안정적이고 안전한 상태가 되게 정해 주어야 한다.

Count라는 속성을 가진 객체가 있는 경우 생성자에서 Count를 0으로 초기화 해야 한다.

속성초기화 : 구조적 프로그래밍에서는 Init으로 쓰인다. 시스템 기본값에 의존하지 말 것

3.1.3. 객체지향의 3단계

클래스를 정의한 코드 그대로일 때 이 클래스의 일종의 부류체(class)라고 부른다면 이 부류체는 생성자(constructor)에 의해서 구성체(construct)가 되고, 이 구성체는 다시 사례체(instance)가 되어 활용된다.

즉 객체지향 프로그래밍을 할 때 부류체, 구성체, 사례체로 이어지는 3단계를 거친다고 할 수 있다.

우리가 보통 객체를 설계한다고 할 때는 부류체(class)를 정의한다는 말이며, 객체를 생성한다고 할 때는 구성체(construct)가 된다는 말이며, 인스턴스를 만든다고 할 때 사례체(instance)가 된다는 말이다.

우리가 부류체를 말할 때 붕어 빵 틀에 비유를 하는데 사실 붕어 빵 틀에 대한 설계도라고 하는 것이 더 적합하다.

여기서 나온 구성체야 말로 진짜 붕어 빵 틀이다.

3.1.4. 기본 생성자

클래스를 작성하여 생성자를 두지 않더라도 컴파일, 활용이 가능한 이유는 클래스는 명시적으로 생성자를 두지 않아도 기본 생성자가 제공되기 때문이다.

생성자를 직접 작성하던지 않던지 간에 항상 생성자는 한 개 이상 있다는 점을 이해해야 한다.

기본생성자가 취하는 유일한 작업은 슈퍼클래스의 생성자를 호출하는 것이다.

public Cabbie()
{
    super();
}

컴파일러가 생성한 바이트 코드를 역으로 컴파일 해보면 이 코드가 표시된다.

기본 생성자가 있다고 해서 충분하다고 생각할 수 있지만 대부분 경우에 어떤 메모리 초기화가 수행되어야 한다.

상황에 관계없이 클래스에 항상 생성자를 한 개 이상 두는 게 좋은 프로그래밍 습관이다.

3.1.5. 다중 생성자 사용

많은 경우에 여러 방식으로 객체를 생성할 수 있다.

이는 메서드 오버로딩을 통하여 가능하다.

public Cabbie()
{
    super();
}

public Cabbie(String name)
{
    super();
    this.name = name;
}

3.1.6. 메서드 오버로딩

메서드의 시그니처가 매번 다르면 오버로딩을 통해 프로그래머는 동일한 메서드 이름을 계속해서 사용할 수 있다.

시그니처는 메서드 이름과 매개변수 목록으로 합성된다.

// 시그니처
public String GetRecord(int key)

// 시그니처 = GetRecord + (int key)
// 시그니처 = 메서드 이름 + 매개변수 목록

시그니처는 언어에 따라 반환 형식을 시그니처의 일부로 보기도 하고 그렇지 않기도 한다.
자바 및 C#에서는 반환 형식을 시그니처의 일부로 보지 않는다.

즉, 시그니처를 서로 다르게 하면 생성자별로 다른 객체를 생성할 수 있다.

물론 같은 생성자를 사용하더라도 본질은 다른 객체이긴 하다

이 기능은 사용 가능한 정보의 양을 미리 알 수 없는 경우에 아주 유용하다.

3.1.7. 슈퍼클래스가 생성되는 방법

상속을 사용할 때는 부모 클래스가 어떻게 구성(construct)되어 있는지를 알아야 한다.

상속은 부모에 관한 모든 것을 상속받는다.

상속을 사용할 땐, 부모의 모든 데이터와 행위를 철저히 알고 있어야 한다.

속성의 상속은 아주 분명하다.

그러나 생성자가 어떻게 상속되는지는 분명하지 않다.

  • 생성자 안에서 클래스의 슈퍼클래스의 생성자가 호출된다.
    • 명시적으로 호출하지 않으면 기본 생성자가 자동으로 호출된다.
    • 바이트 코드에서 해당 생성자 코드 부분을 볼 수 있다.
  • 객체의 각 클래스 속성이 초기화된다.
    • 이들은 클래스 정의의 일부인 속성이며, 생성자 내의 속성이나 그 밖의 메서드가 아니다.
  • 생성자의 나머지 코드가 실행된다.

3.1.8. 생성자의 설계도

클래스를 설계할 때는 모든 속성을 초기화하는 편이 바람직하다.

일부 언어에서는 컴파일러가 초기화를 제공하지만 그것을 맹신하면 안된다.

자바는 초기화 이전에 속성을 사용할 수 없다.

생성자는 애플리케이션이 안정적인 상태인지 확인하는 데 사용된다.

설계하는 동안에 모든 속성의 안정 상태가 무엇인지를 식별한 다음에, 생성자에서 이 안정 상태로 초기화하는 편이 바람직하다.

3.2. 오류 처리

클래스를 처음부터 아주 완벽하게 작성하기는 아주 힘들다.

전부는 아니더라도 대부분의 상황에서 문제가 생길 것이다.

문제가 발생할 때에 대한 대비를 미리 해 두지 않으면 재난적인 상황이 벌어질 수 있다.

여러가지 오류처리 방법이 있다.

문제를 바로 수정하거나, 오류억제로 문제를 무시하거나, 런타임 도중 우아한 방법을 통해 오류를 퇴출하는 것.

  • 문제를 무시하는 건 좋은 생각이 아니다.
  • 잠재적인 문제를 확인하고 문제를 발견하면 프로그램이 중단되도록 하자.
  • 잠재적인 문제를 확인하고 실수를 파악한 후 문제를 해결하자
  • 예외를 던진다.

3.2.1. 문제를 무시하지 않기

모든 애플리케이션의 주요 지침을 들자면 애플리케이션이 중단되지 않아야 한다는 점을 들 수 있다.

오류를 처리하지 않으면 결국 애플리케이션이 비정상적으로 종료되거나 불안정한 상태로 간주될 수 있는 상태로 계속 진행한다.

후자의 경우 잘못된 부분이 있다는 사실도 모를 수 있어서 더욱 나쁜 상황이다.

게임 도중에 문제가 발생하여 강제 종료되는 경우

3.2.2. 문제를 점검하고 애플리케이션을 중단하기

잠재적인 문제를 감지함으로써 애플리케이션을 중단할 때는 애플리케이션에 문제를 나타내는 메시지를 표시할 수 있다.

이 경우 애플리케이션이 정상적으로 종료될 것이고 사용자는 문제를 파악할 수 있다.

문제를 무시하는 방법보다 더 나은 처리 방법이긴 하지만 가장 좋은 방법은 아니다.

게임 도중에 서버점검 메세지가 뜨고 종료되는 경우

3.2.3. 문제를 점검해 복구하기

잠재적인 문제를 확인하고 실수를 포착해 복구해 보려는 방식은, 단순히 문제를 확인하고 애플리케이션을 중단시켜 버리는 방식보다 훨씬 뛰어난 해법이다.

서버의 데이터를 받아오기 실패했을 때 다시 받아오는 시도의 경우

3.2.4. 예외 던지기

대부분의 객체지향 언어는 예외라는 특징을 제공한다.

가장 기본적인 의미에서 예외란 시스템 내에서 발생하는 예기치 않은 이벤트다.

try {
    // 예외가 발생할 수 있는 코드
} catch (Exception e) {
    // 예외를 처리하는 코드
}

try블록 내에서 예외가 발생하면 catch 블록이 예외를 처리하고, 블록이 실행되는 동안 예외가 발생하면 다음과 같은 일이 일어난다.

  1. try 블록의 실행이 종료된다.
  2. catch절은 위반으로 인한 예외를 처리하는 데 적합한 catch절을 찾는다.
  3. catch절 중 어느 것도 문제가 되는 예외를 처리하지 않으면 다음 차례에 해당하는 부모 수준의 try블록으로 전달된다.
  4. 일치하는 catch절이 있다면 catch절이 실행된다.
  5. 그런 다음 try블록의 다음 차례로 나오는 명령문으로 살행이 재개된다.

방어 코드(bulletproof code): 여기에 설명된 방법을 조합해 프로그램을 가능한 사용자에게서 강력히 보호하는 게 좋다.

3.3. 범위의 중요성

클래스 한 개로부터 여러 객체를 인스턴스화할 수 있다.

이러한 각 객체에는 고유한 ID와 상태가 있다.

이게 핵심이다.

각 객체는 개별적으로 생성되며, 자체 메모리에 할당된다.(디버깅으로 해당 ID값을 볼 수 있음)

그러나 일부 속성 및 메서드는 올바르게 선언된 경우에 동일한 클래스에서 인스턴스화된 모든 객체가 공유할 수 있으므로 이러한 클래스 속성 및 메서드에 할당된 메모리를 공유할 수 있다.

공유 메서드: 생성자는 클래스의 인스턴스가 공유하는 메서드의 좋은 예이다.

메서드는 객체의 행위를 나타낸다.

객체의 상태는 속성으로 표현한다.

속성의 종류는 세 가지다.

  • 지역적인 속성
  • 객체의 속성
  • 클래스의 속성

3.3.1. 지역적인 속성

특정 메서드를 사용해 지역적인 속성들을 지닐 수 있다.

public class Number{

    public Method1(){
        int count;
    }

    public Method2(){
    
    }
}

method1 메서드에는 count라는 지역 변수가 있다.

이 정수형 변수에는 method1 내부에서만 접근이 가능하다.

method2 메서드는 정수형 변수 count가 존재하는지도 모른다.

이 시점에서 매우 중요한 개념인 **범위(Space)**를 소개한다.

속성은 특정 범위 안에 들어 있게 된다.

이 경우에 정수형 변수인 count는 method1이라는 범위 안에 들어 있게 된다.

클래스 자체에도 범위가 있고 메서드 마다의 각 범위가 있다.

3.3.2. 객체의 속성

많은 설계 상황에서 동일한 객체 내의 여러 메서드들이 속성을 공유해야 할 때가 있다.

한 객체에 멤버 변수, 즉 인스턴스 변수로 설정된 count가 존재한다면 이는 해당 클래스에 존재하는 메서드에서 참조가 가능하다.

여러 객체를 생성한다면 각각 객체마다 고유한 메모리를 할당받아 count라는 변수를 각각 가지게 된다.

이런 경우 각 객체의 속한 count는 해당 객체의 속성이라고 정의한다.

중복된 식별자라도 범위가 다르면 다른 속성이 된다.

객체의 속성 this로 호출

3.3.3. 클래스의 속성

동일한 클래스를 가지고 만든 두 개 이상의 객체들끼리 서로 속성을 공유할 수 있다.

정정 형식(Static)으로 선언한다.

static을 사용하면 클래스에서 인스턴스화된 모든 객체에 대해 단일 메모리 속성이 할당된다.

따라서 클래스의 모든 객체는 count를 가리키는 동일한 메모리 위치를 사용하게 된다.

정적 속성은 사이드 이펙트를 유발할 수 있기 때문에 잘 사용되지 않는다.

3.4. 연산자 오버로딩

일부 객체지향 언어를 사용하면 연산자를 오버로딩할 수 있다.

오버로딩은 강력한 메커니즘이지만 코드를 읽는 사람이나 유지보수하는 사람들에게 혼동을 줄 수 있다.

3.5. 다중 상속

다중 상속은 클래스 설계의 더 강력하면서도 난해한 측면 중 하나이다.

다중 상속이라는 말을 통해 알 수 있듯이, 다중 상속을 통해 어떤 한 가지 클래스를 자기 자신이 아닌 그 밖의 클래스들 중 두 개 이상으로부터 상속을 받을 수 있다.

실제로도 다중 상속이 좋은 방안인 것 처럼 보이지만, 다른 책에서도 다루듯이 많은 문제점이 있다.

다중 상속은 연산자 오버로딩과 마찬가지로 강력한 기능이다.

어떤 문제는 필수로 필요로하고 세련되게 해결이 가능하다.

하지만 복잡성이 크게 늘어나게 된다.

자바와 닷넷은 이를 생각하여 지원하지 않는다.

행위 상속과 구현부 상속

인터페이스는 행위 상속 메커니즘이며, 추상 클래스는 구현부를 상속하는 데 사용된다.

인터페이스가 담당하는 언어 구성소가 행위적 인터페이스를 제공하면서도 구현부를 제공하지 않는 반면에, 추상 클래스들은 인터페이스와 구현부를 둘 다 제공할 수 있다.

3.6. 객체 연산

우리가 복잡한 자료구조나 복잡한 객체를 다뤄야 한다면, 프로그래밍에서 가장 기본적인 연산자들일지라도 그중에 어떤 것들은 점점 더 복잡해져야 한다.

예를 들어, 기본 데이터 형식을 복사하거나 비교하는 과정은 무척이나 간단하다.

그러나 비교하는 일은 그리 간단하지 않다.

  • 클래스와 참조

복잡한 자료구조 및 객체의 문제점은 참조가 포함될 수 있다는 점이다.

단순히 참조만 복사한다면 참조 대상인 자료구조나 객체는 복사되지 않는다.

같은 맥락에서 객체를 비교할 때 포인터를 그 밖의 포인터와 단순하게 비교만 한다면, 실상은 포인터가 가리키는 것을 비교하는 게 아니라 참조를 비교하는 셈이 되고만다.


객체들을 대상으로 비교나 복사를 수행할 때는 문제가 발생한다.

특히, 포인터를 따라 가는지 여부에 따라 질문할 내용이 달라진다.

그럼에도 객체를 복사할 수 있는 방법이 있어야 한다.

복사는 간단해 보이지만 유효한 복사를 하려면 참조 트리들을 따라가야하는 복잡함이 있다.

  • 전체 복사 vs 단순 복사

모든 참조를 따라가 참조된 모든 객체에 대해 새 사본을 작성할 때 전체 복사가 이뤄진다.

DeepCopy

한 차례의 전체 복사에 많은 수준이 관련될 수 있다.(부담되는 작업일 수 있음)

단순 복사는 참조를 복사할 뿐 수준별로 처리하지 않는다.

ShallowCopy


이런 문제는 객체들을 서로 비교할 때도 나타난다.

객체를 비교하기 위해선 복사와 마찬가지로 참조 트리를 따라가야 한다.

3.7. 결론

클래스 설계와 같은 상위 수준의 객체지향 개념을 다뤘다.

3.7.1 느낀점

대부분 알고 있던 내용이지만 한번 점검하는 느낌이라 나쁘지 않았다..