포스트

(Day 25) OOP; 추상클래스와 추상메서드, 게터와 세터, 오버로딩 및 오버라이딩

복습

다형적 변수의 사용방법

컴파일러는 형식상 옳은지 아닌지만 검사한다. 자식 타입은 부모보다 변수나 기능이 더 많기 때문에, 자식 타입의 레퍼런스에 부모 클래스의 인스턴스 주소를 할당해 줄 수 없다. 형변환을 통해서 할당한다 하더라도, 부모 인스턴스에서는 자식 클래스의 변수나 메서드가 없기 때문에 런타임 오류가 발생할 것이다. (실제 그 레퍼런스가 사용될 때가 아니라 JVM이 초기 검사를 할 때 바로 오류를 나타낼 것이다.)

다형적번수를 형변환하는 경우?

##

1
a instanceof b

위 문장은 클래스가 동일한지를 검사하는 것이 아니다. 상위/하위 클래스의 인스턴스인지만 보고서 불리언값을 반환한다.

1
2
3
인스턴스.getClass() == 클래스.class
//빌트인변수 중 하나인 class 변수 (스태틱은 상속안한다. 컴파일할 때 내장된다)
//Object 클래스의 getClass() 메서드는 인스턴스의 클래스를 반환한다.

오버로딩

추가 + 적재 같은 이름을 갖는 메서드를 추가로 적재하는 것이 오버로딩 파라미터의 정보가 달라야 한다. (개수와 타입) 파라미터의 이름은 로컬변수의 이름으로 사용되는 것 뿐이다. 왜 쓰나? 프로그래밍에 일관성을 부여하기 위해서다. 하나의 클래스 안에서 오버로딩을 하는 것도 가능하지만 슈퍼클래스를 상속받은 서브클래스에서 슈퍼클래스의 메서드명에 다른 파라미터 타입과 순서, 개수로 오버로딩하는 것도 가능하다.

오버라이딩

오버라이딩은 덮어+쓰다 이다. 자식클래스에서 상속받은 메서드를 재정의하는 것이 오버라이딩이다.

예시) Score 클래스가 있다. 국, 영, 수 점수를 변수로 가지고 있다. 그리고 이 점수의 합계와 평균을 계산해주는 메소드 둘을 가지고 있다. 이 Score 클래스를 확장하여(상속하여) 만든 클래스가 있다. 여기에는 음, 미, 체 점수 변수가 추가되었다. 합계와 평균을 계산하는 메서드는 음, 미, 체 점수를 반영해야 한다. 이를 위해 메서드를 새로 바꾸지 않고, 재정의하기 위해 오버라이딩이 사용될 수 있다.

this와 super 레퍼런스를 통해 필드나 메서드를 사용하는 방법을 아는가?

this = 실제 인스턴스 클래스부터 찾아올라간다. (상위 클래스로) super = 메서드가 소속된 클래스부터 상위 클래스로 찾아 올라간다.

필드 오버라이딩

메서드가 아니라 필드(변수)를 오버라이딩하는 것도 가능하다. 가능하지만, 하지말자. 가독성을 떨어트리고 이해하기 어렵게 만든다. 그럼에도 불구하고 존재하는 문법이므로 어떤 것인지 알아는 보자. 구직시장에서는 변별력을 위해 사용될 수 있는 지식이기 때문에..

1
2
3
4
5
6
7
8
9
A  obj  =  new  A2();
obj.m();  // A2의 m() 호출.
// 레퍼런스가 하위 클래스의 인스턴스를 가리킬 때,
// => 레퍼런스를 통해 호출하는 메서드는
// 레퍼런스가 실제 가리키는 하위 클래스에서 찾아 올라 간다.
// 그렇다고 해서 A2에서 추가한 메서드를 호출할 수는 없다.
// => 즉 레퍼런스의 클래스를 벗어나서 사용할 수는 없다.
// 컴파일러가 허락하지 않는다.
// obj.x(); // 컴파일 오류! 컴파일 시에는 레퍼런스의 데이터타입인 클래스 A에 해당 메서드나 필드가 있는지 따져본다.

final 키워드의 사용법

클래스에 final을 붙일 때는?

더 이상 상속이 불가해진다. 객체를 파라미터로 받을 때, 서브클래스를 받고 싶지 않을 때 쓴다.

메소드에 final을 붙일 때는?

더 이상 오버라이딩이 불가능해진다. 확장(상속)한 다른 클래스에서 메서드를 더이상 재정의하지 못하게 하고 싶을 때 쓴다.

Parameter에는 final 을 사용하자

왜? 원래 받은 파라미터 값은 다시 사용될 가능성이 매우 높다. 메서드 내부에서 그 값을 변경하면 원래 받은 값을 쓸 수가 없어진다. 그래서 final을 붙여서 보호해두는 것이다.

변수에 final을 붙일 때?

상수를 저장하고 그것을 변하게 하면 안될 때. 인스턴스변수에는 당연 final 붙일 일이 없으나 스태틱 변수에 사용한다.

### 조언: 객체지향 프로그래밍의 핵심능력 문법을 사용해서 구조를 만들어내는 것이 중요함. 문법을 암기하는 것이 중요한 것이 아님. 문법이 생겨난 이유와 그걸 어떻게 활용하는지가 중요함. 그래서 급하게 갈 수가 없음.

추상 클래스에 대해서

추상클래스는 인스턴스를 생성하기 위한 것이 아니다. 상속만을 위한 클래스이다. 인스턴스 생성이 불가능하다.

추상 메서드는 추상 클래스에서 사용한다. 마찬가지로 상속 문법을 위한 것인데, 반드시 구현해야 하는 메서드를 구별해주기 위해 사용한다. 추상 클래스를 상속받되, 추상 메서드를 구현하지 않으려면, 상속받는 클래스도 추상 클래스여야만 가능하다.

인스턴스로 만들 수 있는 건 abstract 에 반대되는 의미로 concrete 라고 부른다.

참고로 추상메서드를 재정의하는 것도 오버라이딩(Overriding) 이라는 용어로 지칭 가능하다

추상 클래스에서, 아예 구현하고 싶은 메서드들은 클래스들을 구현해두고, 나중에 상속받은 클래스에서 구현하게 하고 싶은 메서드들은 추상 메서드로 리턴타입 이름, 파라미터(메서드 시그너처)만 알려주는 것이다.

추상클래스와 추상메서드 활용에 관해

Template Method Patter (GoF)

두 클래스가 있다. 버블 정렬, 퀵 정렬을 구현한 두 개의 클래스가 있다. 이 두 클래스는 객체의 사용법이 다르다. 정렬을 실행하기 위한 메서드의 이름과 파라미터가 다르다. 메서드 시그너처가 다르다.

이것을 어떻게 하면 더 편하게 쓸 수 있을까? 그 메서드들을 바꿀 수는 없다고 하자.

추상클래스와 추상메서드를 몰랐다면 일단 공통기능을 상속으로 추출할 것이다. 즉, 슈퍼클래스를 생성하여 일반화를 적용한다는 것이다.

그래서 Sort라는 슈퍼클래스를 만들어서 일반화를 했다. 오버라이딩을 통해 sort()라는 메서드에서 서브클래스들이 각각의 정렬기능을 시작하기 위한 메서드를 실행하도록 한것이다.

그런데 문제가 발생한다. Sort 클래스는 인스턴스로 만들어서 사용하기 위한 것이 아니라, 일반화를 적용하여 관리를 더 낮은 비용으로 하기 위함이었다. 그러나 이 Sort 클래스를 Instance로 만들어서 그 안의 sort() 메서드를 실행하는 것은 문법적으로 오류가 없다. sort()클래스는 재정의하기 위해 슈퍼클래스에서 만든 메서드이니, 실행해도 아무런 일이 일어나지 않는다…

그래서 추상 클래스 문법이 생겼다. 슈퍼클래스로서의 역할은 하지만 인스턴스로 만드는 것을 제한하고 싶을 때 추상 클래스를 사용한다. 이러한 경우는 일반화 과정에서 자주 발생한다.

그런데 추상 클래스도 약점이 있다.

다른 개발자가 MergeSort 클래스도 정렬에 활용하기 위해 추가했다. MergeSort 클래스 또한 Sort 클래스를 상속받았다.

MegerSort 를 추가한 개발자는 같은 이름의 sort() 메서드를 작성했다. 그러나 아쉽게도, 슈퍼클래스를 주의깊게 보지 않았다. 그리고 우연히도, sort() 메서드와 동일한 이름의 메서드를 작성했다. 그 메서드는 파라미터의 개수가 달랐다… 오버라이딩(재정의) 한 것이 아니라 그냥 새로운 메서드를 가진 것이다. 기존에 사용하던 코드와 호환되지 않는…

추상클래스는 서브클래스의 메서드 오버라이딩을 강제할 수 없다.

그래서 서브클래스의 메서드 오버라이딩을 강제하기 위해, 추상 메서드 문법이 구현된 것이다.

그래서 추상 메서드 문법이 생겼다.

인터페이스(Interface)

만약 위 추상클래스와 추상 메서드 같이, 서브클래스의 사용법 통일을 위한 슈퍼클래스는 실제로 상속해주는 구현이 하나도 없다. 즉, 필드나 구현된 메서드를 서브클래스에게 넘겨주는 것이 없다는 것이다. 사용 규칙만 정의하는 것이고.

이 때는 추상클래스를 쓰지 말고 인터페이스를 쓰는 것이 더 낫다. (인터페이스임을 알면 사용 목적을 더 편하게 알 수 있다!) 인터페이스로 선언한 경우에는, 자동으로 메서드에 abstract 키워드가 붙는다. 서브클래스들은 extends 가 아닌 implements 키워드를 쓴다.

Getter & Setter 게터와 세터

게터와 세터는, 개발자가 클래스를 잘못 쓰는 것을 방지하기 위한 것이다. 인스턴스 내부의 필드에 직접 접근하는 것은 문제가 될 수 있다. (Score 클래스에서 점수를 계산해둔 펑균과 합계를 생각해보자)

그래서 필드에 직접 접근이 불가능하게 private 접근제어자를 쓰게 된다. 여기가 출발이다.

그럼 그 필드의 값을 보고 싶은데 어쩌나? private는 같은 클래스의 멤버들만 접근이 가능하므로, 같은 클래스에 메서드를 만들어서 그 값을 반환하도록 한 것이다. 그것이 게터이다. 게터 메서드는 이름을 보통 get필드명() 으로 짓는다.

때때로 private 필드의 값을 변경해야 할 일도 있을 것이다. 이 경우도 Score 클래스를 생각해보면, 값을넣고 나서 평균과 합계를 계산해야 할 것이다. 이것을 실수로 놓칠 가능성이 높다. 마찬가지로 public 메서드를 하나 만들어서 값을 설정하게 하고, 평균과 합계를 계산하는 부분도 메서드 호출시 실행되도록 한다. 세터에서는 할당하려는 값의 유효성을 확인할 수도 있다. 값을 바꿀 때 필요한 다른 작업들이 있는 경우 이러한 getOOO()메서드를 이용하여 더 견고한 프로그램을 만들 수 있다.

세터 메서드의 코드들은 필요 이상으로 더 많은 작업들을 하게 될 수 있다. 세터 메서드에는 단순히 값만 설정하게 하고, 필요한 부수작업들은 (합계와 평균의 계산) 나중에 한번에 몰아서 처리하게 하고 싶을수도 있다.

이는 trade-off 관점으로 생각해야 한다. 개발자의 실수를 방지할 수 있다면(=유지보수 비용을 줄일 수 있다면) 성능상의 불이익을 감수하겠다는 것이다.

접근제어자에 관해

공개할 필요가 없으면 메서드나 필드를 private으로 접근제어 하는 것이 더 나은 방법이다. access controller 라고 불릴 것 같지만, 영어에서는 modifier라고 불린다.

게터와 세터의 사용에 대해

실제로는, 거의 모든 필드를 private 또는 protected 접근제어자로 캡슐화한 다음에, 게터와 세터 메서드를 이용하여 그 값을 받고 설정한다.

왜?
그러면 프로그래밍에 일관성이 생겨서 코드 작성에 직관성이 생긴다.

그래서 실무에서 거의 모든 필드가 private 또는 protected 접근제어자로 캡슐화되어있고 게터와 세터를 둔다. 세터에서 값의 유효성 잡아주지 않더라도 그렇게 한다. 어떤 변수는 레퍼런스.변수명으로 접근하고, 어떤 변수는 get변수명() 으로 접근하면 일관성이 없다는 것이다. 일관성이 없으면 직관성에서 손해를 보게 된다.

코드를 작성하는 것은 사실 가장 비용이 적은 일이다. 통일성을 잃는 것은 아주 큰 비용을 초래하는 일이다. 왜 getter, setter가 존재하는지는 이제 알았으니, 일관성을 위해 클래스 필드에 대한 접근은 게터와 세터를 사용하도록 하자.

이것은 통일성을 만들어서, 일관성을 부여하고, 그로인한 직관성을 얻기 위함이니, 입문자들은 고민하지 말고 필드를 캡슐화하고, 게터와 세터를 사용하자.

그리고, 혹시라도 게터, 세터의 바디에 새로운 코드를 추가해야 할 때가 생길 수 있다. 연관된 코드가 많으면 많을수록, 게터 세터로 쓴 것이 기능확장에 더 유리하다.

Property

게터와 세터는 Property라고도 불린다! 필드에서 get, set 할 수 있다면 read/write property 라고 불린다. get 만 있으면 read only property 라고 불린다. set 만 있으면 write only property 라고 불린다.

프로퍼티는 게터와 세터를 통해 접근되는 객체의 속성을 나타냅니다. 객체의 상태를 나타내는 속성(property)은 종종 해당 객체에 속하는 것처럼 느껴지므로, 게터와 세터를 통해 접근되는 멤버 변수를 프로퍼티라고 부르기도 합니다.

캡슐화(Encapsulation)

캡슐화는 인스턴스의 변수에 추상화 목적에 맞는 유효한 값만 넣을 수 있도록 외부 접근을 제한하는 문법이다.

사람 클래스가 있고, 이 클래스에는 나이와 키 변수가 있다고 하자. 그런데 나이에 200살을 적고, 키에 -30 을 적으면 그것이 유효한 인스턴스일까? 이런 경우는 추상화가 무너진 것이다. 적합하게 분류하지 못하고 있는 것이다. 인스턴스의 유효성을 잃은 것이다.

private으로 접근을 제어하고, 세터를 통해서만 값을 설정할 수 있게 한다면 이런 필드의 유효성을 검사할 수 있다. 세터 메서드에 값의 유효성을 검사하는 코드를 추가하여, 값을 설정할 때마다 유효성 검사를 하게 할 수 있기 때문이다. 다만 실무에서는 세터에서 유효성검사를 하는 일은 별로 없다.

유효성 검사에 대한 실무

실제로는 게터 세터에서 유효성 검사를 하는 일이 많지 않다. 유효성 검사를 하는 메서드를 따로 만드는 것이 보통이다. 그럼에도 불구하고 게터와 세터를 쓰는 이유는,

  1. 필드 접근을 메서드를 통해 하는 것이 더 선호된다.
  2. get, set 할 때 코드를 추가해야 할 일이 생길 때, 다른 코드를 수정하지 않고 코드 추가가 가능하다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.