220114 - TIL

Suntory·2022년 1월 14일
1

TIL

목록 보기
9/57

클래스와 오브젝트, 인스턴스

  • 클래스: 현실에 존재하는 개체들 중 어떤 동일한 특성을 추상화하여 공유하는 개체들을 대표하는 틀이라고 생각합니다.
    이를테면 사람들 중에서 현재 일하는 직종에 따라 학생, 엔지니어 등으로 나눌 수 있을 것입니다. 추가로 어느 학교를 다니는 지에 따라 대학생, 고등학생 등으로도 나눌 수 있습니다. 객체들의 속성 뿐만 아니라 그 객체들이 공유하는 행동에 관한 것도 담고 있습니다.
  • 오브젝트 : 소프트웨어로 만들어내고 싶은 대상 그 자체라고 생각합니다. 클래스가 틀이라면 오브젝트는 그 클래스의 속성을 가지는 알맹이라고 할 수 있습니다.
  • 인스턴스 : 각 객체를 실제로 실체화시켜 메모리상에 만든 개념이라고 생각합니다. 그 전까지의 오브젝트는 개념만 존재했다면 인스턴스화하면 실체화되어 메모리상에 클래스와 관련된 메모리구조를 갖게 됩니다.

상속과 다형성

  • 상속 : 객체 지향 프로그래밍에서는 한 클래스가 다른 클래스를 상속할 수 있습니다. 집합 관계처럼 한 클래스가 다른 클래스를 포함하고 있거나 상위 분류에서 하위 분류로 갈라져나가는 등의 클래스 구조를 만들 수 있습니다. 이 때, 상속을 받게 된 부모 클래스의 동작이나 속성을 자식 클래스가 사용할 수 있게 되는데 이것이 상속입니다. 부모 클래스와 자식 클래스가 공유하는 함수가 있다면 반복해서 짜지 않고 그대로 사용할 수 있게 됩니다.
  • 다형성 : 동일한 이름의 메소드로 객체에 따라, 또는 대상에 따라 다른 동작을 수행하게 만드는 것을 의미한다고 생각합니다. 현실 세계에서도 객체가 똑같은 행동을 하더라도 이용하는 도구, 대상에 따라 과정이 달라질 수 있습니다. 예를 들어 '마시다'라는 동사가 있다고 생각해봅시다. 어떤 사람은 제로콜라를 마시고, 어떤 사람은 아이스 아메리카노를 마십니다. 그럼 제로콜라를 마시는 사람은 캔을 따고, 캔에 입을 대고 마십니다. 하지만 아이스 아메리카노를 마시는 사람은 빨대로 음료를 빨아먹습니다. 이와 같이 하나의 행동을 다양한 형태로 나타낼 수 있습니다. 소프트웨어의 객체도 마찬가지입니다. 다음 예제 코드를 봐주세요.
    class Drinks {
        private Coffee coffee;
        private Coke coke;

        public void drink(Coffee coffee) {
            shakeByStraw();
            drinkByStraw();
        }

        public void drink(Coke coke) {
            pickCan();
            drinkDirectly();
        }
    }

위의 예시를 간략하게 구현한 코드입니다. Drinks 클래스 내부에 정의된 drink라는 함수는 인자로 어떤 종류의 음료를 받느냐에 따라 실행되는 내부 로직이 달리 구현되어 있습니다. 이를 메소드 오버로딩이라 하고 다형성의 대표적인 구현 방법이다.

추가로, 부모 클래스로부터 상속을 받은 메서드의 기능을 수정하고 싶을 수 있습니다. 이 때, 부모 클래스를 직접 수정하게 되면 그 메서드를 상속받고 있는 나머지 클래스에도 영향을 미치게 됩니다. 이런 경우 부모가 상속해준 메서드를 자식 클래스에서 수정하여 사용할 수 있습니다. 이것을 메소드 오버라이딩이라고 합니다.

public abstract class Shape {

    public abstract double getProperty();
}

public class Polygon extends Shape {

    @Override
    public double getProperty() {
        return Calculator.calculatePolygonArea(pointCollection);
    }
}

메소드 오버라이딩을 적용한 예제 코드입니다. Polygon이라는 객체는 Shape를 상속받고, Shape에서 정의한 getProperty 함수를 상속받습니다. (이 경우 추상클래스의 추상메서드이기 때문에 반드시 상속받아서 오버라이드해야 하는 경우입니다.) 만약 추상클래스가 아니였더라도 Polygon에서 해당 메소드를 오버라이딩하여 동작하기 바라는 기능을 구현할 수 있습니다.
즉, 부모와 자식이 같은 메소드를 사용하지만 동작하는 방식이 다르므로 다형성을 구현할 수 있는 방법입니다.

this와 super 키워드

  • this : 참조 변수로 인스턴스 자신을 지칭하는 키워드입니다. 예를 들어 아래와 같은 생성자가 있다고 합시다.
class ThisExample {
	private int age;
    
	public ThisExample(int age) {
    		this.age = age;
    }
}

위 클래스의 생성자는 age라는 변수를 받아 인스턴스 멤버인 age를 초기화해주는 역할을 합니다. 그런데, 매개 변수의 이름과 인스턴스 변수 이름이 age로 같은 상황입니다. 그래서 age라는 멤버 변수를 가리키기 위해 this라는 키워드를 사용합니다. 또한, this()라는 것은 현재 클래스의 생성자를 의미합니다. 또한, 인스턴스를 가리키고 있기 때문에 클래스 변수에 대해서는 접근이 불가능합니다. 모든 인스턴스 메서드에는 자신과 연관된 인스턴스를 가리키는 참조변수 this가 지역변수로 숨겨진 채로 존재합니다.

  • super : 현재 클래스의 부모 클래스를 나타내는 키워드입니다. 예를 들어, 메소드 오버라이딩을 거쳐서 부모 클래스와 동일한 이름의 메소드가 이미 존재한다고 합시다. 예제 코드는 다음과 같습니다.
// 부모 클래스
protected void setAlarm() {
	Alram.create("07:00");
}

부모 클래스에서 7시에 알람을 맞추는 함수를 만들었습니다.

// 자식 클래스
@override
protected void setAlarm() {
	Alram.create("08:00");
}

자식 클래스는 이 함수를 오버라이드하여 8시에 알람을 맞추기로 하였습니다. 하지만 부모 클래스의 함수가 중요하여 항상 7시에는 알람을 맞춰야 한다면 어떻게 해야 할까요? 오버라이드를 할 때, 부모 클래스의 함수 또한 호출하면 됩니다.

// 자식 클래스
@override
protected void setAlarm() {
	super.setAlram();
	Alram.create("08:00");
}

super 키워드를 이용하면 상속하고 있는 부모 클래스에 접근할 수 있고, 부모 클래스의 setAlarm을 호출함으로써 7시와 8시에 알람을 설정할 수 있게 됩니다.
this와 마찬가지로 super()는 부모클래스의 상속자를 뜻합니다. 또한 static 메서드나 멤버에 대해서는 접근할 수 없습니다.

객체와 인스턴스

  • 객체 : 클래스가 대표하는 현실 세계의 객체 같은 느낌입니다. 구체적인 대상이 아닌 추상적으로 느껴집니다.
  • 인스턴스 : 메모리상에 객체의 특성을 가지고 올라온 객체를 말한다고 생각합니다. 클래스에서 파생된 구체적인 대상으로 인식합니다.

SOLID 원칙(참고 링크)

  • SRP (단일책임의 원칙: Single Responsibility Principle)
    : There should never be more than one reason for a class to change.

    어떤 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어야 한다는 원칙입니다. 어떤 변화가 생겨 클래스를 변경해야 한다면, 그 이유가 오직 하나이어야 합니다. SRP 원리를 적용하면 무엇보다도 각 클래스의 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임에 끼치는 연쇄작용을 피할 수 있습니다.

이를 적용하기 위해 어떤 클래스를 쪼갬으로써 각 책임을 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 합니다. 책임만 분리하는 것이 아니라 분리된 두 클래스간의 관계의 복잡도를 줄이도록 설계합니다. 만약 쪼갠 클래스 각각이 유사하고 비슷한 책임을 중복해서 갖고 있다면 공통된 Superclass를 만드는 것도 방법입니다. 각 클래스에서 공유되는 요소를 부모 클래스로 정의하여 부모 클래스에 위임하는 기법입니다.

  • OCP (개방폐쇄의 원칙: Open Close Principle)
    : You should be able to extend a classes behavior, without modifying it.

    소프트웨어의 구성요소(클래스, 함수)등은 확장에는 열려있고, 변경에는 닫혀있어야 한다는 원리입니다. 기존 구성요소에 어떤 요소가 추가될 때 들어가는 비용을 가능한 줄일 수 있는 방법입니다. 변경이나 추가가 일어나더라도 기존 구성요소의 수정없이 추가되어야 하며 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻입니다.

이를 적용하기 위해 어떤 요소를 만들 때 다음과 같은 원칙으로 생각해봅니다.
1. 변경될 것과 변하지 않을 것을 엄격히 구분합니다.
(예를 들어 고유 정보 등의 변하지 않는 식별 정보, 수학 공식 등에 필요한 상수 vs 변동되는 가격정보, 공식에 사용되는 변수)
2. 이 두 모듈이 만나는 지점에 인터페이스를 정의합니다.
3. 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성합니다.

  • LSP (리스코브 치환의 원칙: The Liskov Substitution Principle) : Functions that use pointers or references to base classes must be able to use object of derived classes without knowing it.

요약하자면, 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 입니다. 상속한 클래스가 슈퍼 클래스의 타입으로 언제든지 대체될 수 있다는 것입니다. 즉, 슈퍼 클래스에서 제공하는 인터페이스를 어겨서는 안됩니다. 상속은 궁극적으로 다형성을 통한 확장성 획득을 목표로 합니다. 다형성과 확장성을 극대화하려면 하위 클래스를 사용하는 것보다는 상위의 클래스(인터페이스)를 사용하는 것이 더 좋습니다. 일반적으로 ArrayList를 선언할 때 아래와 같이 선언은 상위 클래스로, 생성은 구체 클래스로 하는 것을 알 수 있습니다.

List<> newList = new ArrayList<>();

즉, 이 객체는 내부적으로 ArrayList 생성자를 통해 생성되지만 타입은 업캐스팅하여 List<>입니다. 처음에 ArrayList로 생각하고 설계를 진행하다가 자료의 추가/삭제가 빈번해 LinkedList로 변경하고 싶다면 어떻게 해야 할까요? 만약 ArrayList로 선언했다면 리턴 타입이나 매개 변수 등을 전부 수정해야 합니다. 그러나 만약 List로 업캐스팅하여 코드를 작성했다면 단지 선언부만 LinkedList로 바꾸면 됩니다. 이처럼 객체는 인터페이스를 통하여 선언하는 것이 변경에 더 유연하다는 것을 알 수 있었습니다.

이를 적용하기 위해 다음과 같은 원칙을 생각해봅니다.
1. 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둡니다.
2. 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 인터페이스 상속하여 구현합니다.
3. 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만듭니다.
4. 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현상속을 사용합니다.

  • ISP (인터페이스 분리의 원칙: Interface Segregation Principle) : Clients should not be forced to depend upon interfaces that they do not use.

이 원리는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리입니다. 즉 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 합니다. 즉, 일반적인 하나의 인터페이스보다는 구체적인 여러개의 인터페이스가 낫다라고 정의할 수 있습니다. 특정 클래스를 이용하는 클라이언트(서브 클래스)가 여러개고, 그 클래스의 특정 부분만 이용한다면, 그 부분을 분리하여 인터페이스로 만들어 사용하는 것을 권장합니다. 일종의 인터페이스의 단일 책임 원칙이라 볼 수 있습니다.

  • DIP (의존성 역전의 원칙: Dependency Inversion Principle)
    • High level modules should not depend upon low level modules. Both should depend upon abstractions.
    • Abstractions should not depend upon details. Details should depend upon abstaractions.

이 원칙은 한마디로 특정하게 구현된 저수준 모듈이 아닌 추상화된 고수준 모듈에 의존하라는 원칙입니다. 개방-폐쇄 원칙과도 밀접한 관계가 있습니다. 예를 들어 어떤 게임 캐릭터 객체에 특정 무기를 담아서 생성한다고 생각해봅시다. 만약 그 캐릭터가 다른 무기를 착용하면 어떻게 해야할까요? 생성자 자체를 수정해야 합니다. 우선 개방-폐쇄 원칙을 위배하고 있습니다. 게다가 특정 무기라는 저수준 모듈을 의존하고 있는 상황입니다. 이를 탈피하기 위해서는 무기 클래스를 아우르는 추상화된 인터페이스가 필요합니다. 그 다음 캐릭터는 그 인터페이스를 생성자로 받아 구체적인 무기 타입을 입력받으면 됩니다.

profile
천천히, 하지만 꾸준히 그리고 열심히

0개의 댓글