레이블이 OOP인 게시물을 표시합니다. 모든 게시물 표시
레이블이 OOP인 게시물을 표시합니다. 모든 게시물 표시

2017년 7월 5일 수요일

[Java] 상속(Inheritance)과 다형성(Polymorphism)

상속(Inheritance)


객체지향프로그래밍(Object Oriented Programming)에서 중요한 개념인 상속(Inheritance)은 말그대로 객체가 다른 객체를 상속하는 개념이다. 객체가 다른 객체를 상속함으로써, 상속받는 객체는 상속하는 객체의 데이터필드, 메소드에 접근해 사용할 수 있다. 자바도 객체지향 프로그래밍 언어인만큼 상속(Inheritance)이라는 컨셉을 적용한 언어이다. 차량 관리 프로그램에서 차량 객체를 만드는 클래스를 생각해보자. 아마 대부분 차량 객체 하나로 다 잘 만들 수 있을것이다. 하지만, 특수 차량이 존재하고, 특수 차량은 그냥 차량과 달리 특수한 관리가 필요하다고 할때, 아마도 대부분의 경우에는 차량용 객체뿐만 아니라 특수차량용 객체가 별도로 필요하게된다. 이 경우에 객체 설계자는 특수차량 클래스를 완전 새로 적고 기능을 추가할수도 있지만, 특수차량 클래스와 "차량"이라는 특징을 공유하는 유사한 클래스, 즉 차량 클래스에서 프로그래밍 해놓은 데이터필드나, 메소드들을 상속하여 가져와서 사용할수도 있다. 이 상속으로 인해서 특수챠량 클래스와 그냥 차량 클래스는 "is-a" 관계를 가진다. 그러니까 모든 "특수차량"은 "차량"이다라는 관계가 된다. 그리고 추후 차량 클래스를 상속하는 대형차량, 소형차량 등의 다른 섭클래스(Sub-Class)들도 같은 관계에 놓이게 된다. 그리고 상속(Inheritance)은 한 세대에서 끝나는것이 아니라 쭉 이어질 수 있다. 즉 최상위 클래스에서 하위 클래스 몇십개로 이어지는것도 가능하다.

섭클래스(Subclass)는 extends 키워드를 이용해 쉽게 선언할 수 있다.


public class SpecialVehicle extends Vehicle{
...
}

extends 키워드와 상속할 클래스명을 붙임으로써 섭클래스는 상속하는 클래스의 메소드와 데이터필드에 접근할 수 있다. 보통 상속받는 클래스는 subclass, derived class, child class라고 불리며 상속하는 클래스는 superclass, base class, parent class라고 불린다. 보통은 수퍼클래스(superclass)와 섭클래스(subclass)라는 용어가 많이 쓰인다. 위 경우에서는 Vehicle 클래스가 수퍼클래스이며, SpecialVehicle 클래스가 섭클래스이고, 섭클래스가 수퍼클래스인 Vehicle 클래스보다 더 많은 기능을 가진다. SpecialVehicle 클래스가 Vehicle 클래스보다 더 많은 데이터필드와 메소드들을 캡슐화하고 있다. 특수차량은 다른 차량들에 장착되지않은 부스터를 장착하고 있다 가정하자. 그렇다면 SpecialVehicle 클래스는 새로운 데이터필드와 메소드를 가지게된다


public class SpecialVehicle extends Vehicle{
   private String boosterModel;
   ...
   public void setBoosterModel(String b){
      this.boosterModel = b;
   }
   ...
}

사실 메소드와 데이터필드를 추가하는 것은 일반 Vehicle 클래스와 다를게 없다. 하지만 boosterModel 데이터필드와 이 필드의 accessor, mutotar 메소드는 오직 SpecialVehicle 객체를 통해서만 접근이 가능하다. 하지만 원래 Vehicle 클래스가 가지고 있는 model, brand 등의 accessor, mutator 메소드는 SpecialVehicle 객체를 통해서도 접근이 가능하다. 그리고 SpecialVehicle 클래스에서는 Vehicle 클래스의 private 데이터 필드들과 메소드들에도 접근이 가능하다. 이때문에 보통 객체 설계시, 수퍼클래스는 보편적인(General) 메소드들과 데이터필드를 가지고, 섭클래스는 조금 특화된(Specialized) 메소드들과 데이터필드를 가지게된다. OOP에서 공통적인 기능들을 모아 수퍼클래스를 만들어 섭클래스의 부담을 줄이는게 상속의 핵심기능이다.

만일 수퍼클래스가 가진 몇몇 메소드들이 현재 클래스에 맞지 않아 현재 클래스에 특화시킬 필요가 있다면 메소드를 오버라이딩(Overriding) 하면 된다. 예를들어 특수차량에는 일반 보혐료와 추가적인 특수 보험료가 붙는다 가정할 때, 메소드를 오버라이딩하여 수정할 수 있다.


//수퍼클래스
public class Vehicle {
 ...
 private double insuranceFee;
 ...
 public double getInsuranceFee() {
  return insuranceFee;
 }
 ...
}

// 섭클래스
public class SpecialVehicle extends Vehicle{
 ...
 private double specialInsuranceFee;
 ...
 public double getInsuranceFee() {
  return super.getInsuranceFee() + specialInsuranceFee;
 }
 ...
}

우선 Vehicle 클래스의 insuranceFee는 private 변수이다. 그러기 때문에 Vehicle 클래스 내부에서 선언된 메소드들만 접근이 가능하다. 그래서 SpecialVehicle 클래스에서는 직접적인 접근이 불가능하다. 대신 간접적으로 accessor 메소드를 이용해 접근해야 한다. 하지만 메소드의 이름이 둘다 일치한다. 그래서 그냥 해당메소드를 부를 경우에는 그 메소드가 수퍼클래스에 존재해도 현재 클래스의 메소드를 우선적으로 호출하기 때문에, 재귀(Recurrence)현상이 일어나게 된다. 참고로 오버라이딩(overriding)되는 메소드는 수퍼클래스의 메소드 접근자가 public일시 같은 접근자를 가지고 있어야된다. 즉 public이면 public 접근자를 가져야 되고, 그렇지 않다면 컴파일시 에러가 발생한다. 여기서 상위 클래스, 즉 수퍼클래스를 가리키는 super 키워드를 이용해서 상위 클래스를 참조하도록 하면 해결이 된다. 다만 super 키워드는 this 키워드와는 달리 현재 객체를 참조하도록하는게 아니라, 컴파일러에게 직접적으로 수퍼클래스의 메소드, 변수를 참조하도록 요청하는 키워드이기 때문에 this와는 달리 생각해야한다.

super 키워들 이용하면 수퍼클래스의 생성자함수를 이용해 섭클래스의 생성자함수를 완료할 수 있다.


public class SpecialVehicle extends Vehicle{
 ...
 private double specialInsuranceFee;
 ...
 public SpecialVehicle(String brand, String model, double insuranceFee, double specialInsuranceFee,...) {
  super(brand, model, insuranceFee, ...);
                this.specialInsuranceFee = specialInsuranceFee;
                ...
 }
 ...
}

이렇게 super 키워드를 이용해 수퍼클래스의 생성자함수 호출도 가능하다. 참고로 만약 섭클래스의 컨스트럭터가 수퍼클래스의 컨스트럭터를 호출하는 구문이 없다면, 컴파일러는 자동적으로 파라미터 없이 수퍼클래스의 컨스트럭터를 호출하며, 수퍼클래스에 파라미터를 받지 않는 컨스트럭터가 없다면 에러가 발생한다

이제 한번 사용해보자.


Vehicle[] vs = new Vehicle[2];
vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5); //일반보험료 30에 특수보험료 100.5
for(Vehicle e:vs){
 System.out.println(e.getInsuranceFee()); 
}
// 44.6
// 130.5

놀랍게도 Vehicle 타입으로 생성된 배열이지만 Vehicle 타입에 종속된 SpecialVehicle 객체도 배열에 저장할 수 있다.
그리고 보험료 출력시 vs[0] 객체는 일반 보험료를 출력했지만, vs[1] 객체는 특수보험료를 포함한 총 보험료값을 출력했다. SpecialVehicle 타입이 아닌 Vehicle 타입 e 객체를 이용해 메소드를 호출했는데, 프로그램이 알맞은 메소드를 호출해 맞는 결과를 가져왔다. 이렇게 e와 같은 객체 변수가 여러 타입을 참조할 수 있는것을 다형성(Polymorphism)이라고 한다. 그리고 프로그램이 실행되는 시점에서 알맞는 메소드를 호출하는것을 Dynamic Binding이라고 한다.


public class SpecialVehicle extends Vehicle{

 private double specialInsuranceFee;
        ...
 public void setSpecialInsuranceFee(double specialInsuranceFee) {
  this.specialInsuranceFee = specialInsuranceFee;
 }
        ...
}

public class Test {
 public static void main(String[] args) {
  Vehicle[] vs = new Vehicle[2];
  vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
  vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5);
  vs[1].setSpecialInsuranceFee(55.5); 
                // 컴파일 에러가 발생한다
 }
}

한가지 주의할점은 vs[1]을 통해서는 SpecialVehicle 클래스의 메소드를 호출하는것은 불가능하다.
vs[1]이 선언된 타입은 Vehicle이지만 setSpecialInsuranceFee 메소드는 SpecialVehicle 클래스에만 존재하기때문에 그렇다. 즉 Vehicle 타입의 객체는 SpecialVehicle 메소드들중 오버라이딩(Overriding)된 것들만 호출 가능하다.

만약 클래스가 상속되지 못하게 하고싶다거나 상속은 되나 메소드를 수정하지 않았으면 좋겠다면 final 키워드를 이용하면 된다. 예를들어 SpecialVehicle 클래스를 가장 하위 클래스로 만들고 싶다면


public final class SpecialVehicle extends Vehicle{
  ...
}
이 경우에는 클래스 상속도 불가능하고 자동적으로 모든 메소드들이 final 속성을 지니게 된다.

만약 상속되는 SpecialVehicle 클래스나, specialize 메소드를 하위클래스에서 오버라이딩하지 않게 하고싶다면

public final class SpecialVehicle extends Vehicle{
  ...
  public final void specilize(int x){
  ...
  }
}
이 경우에는 클래스 상속은 가능하나 해당 메소드를 오버라이딩하는것은 불가능하다.

다른 자료형처럼 상속관계에 놓여있는 자료형끼리도 캐스팅(Casting), 형변환이 가능하다. 예를들면 double 에서 int로 변환하고 싶다면 double 변수앞에 (int)라는 키워드를 놓아주기만 하면 되듯이, Vehicle형 객체를 SpecialVehicle형 객체로 변환하고 싶다면 (SpecialVehicle) 키워드를 앞에 적으면 된다.


Vehicle[] vs = new Vehicle[2];
vs[0] = new Vehicle("Hyundai", "Sonata", 44.6);
vs[1] = new SpecialVehicle("Volvo", "X841", 30, 100.5);
SpecialVehicle a = (SpecialVehicle) vs[1];

우리는 vs[1] 객체를 통해서는 SpecialVehicle 클래스의 모든 기능에 접근하지 못하지만, a 객체를 통해서는 SpecialVehicle 클래스의 모든 기능을 사용할 수 있다. 자바에서 모든 변수는 형(type)을 가진다. 타입은 변수가 참조하는 객체의 종류와 무었을 할수있는지를 알려준다. 그렇기 때문에 Vehicle 타입의 객체인 vs[1]를 통해서는 SpecialVehicle의 메소드를 호출하지 못한것이다. 그리고 만약 String과 Vehicle 타입처럼 아무런 관계에 있지않은 객체끼리 변환을 시도하면 에러를 얻게 된다.

다형성(Polymorphism)


앞의 상속(Inheritance) 예제를 보면 B 클래스가 A 클래스를 상속할시에 "B는 A다(B is A)" 관계에 놓이게 된다. 만약 이 관계가 아니게되면 A 클래스는 B 클래스를 상속하는것이 아니다. 다른 관계가 있다면, 두 클래스가 교체 법칙(Substitution Principle)에 적용된다는것인데, 만약 적용된다면 프로그램이 언제든디 수퍼클래스의 객체를 원할 때, 섭클래스의 객체가 사용될 수도 있게된다. 정리하자면, B 클래스가 A 클래스를 상속할 때, 프로그램이 B 객체를 A 객체처럼 사용할수도 있다는뜻이다. 이는 자바프로그램에서 객체형(Object Type) 변수가 다형적(Polymorphic)이기 때문이다. 그래서 모든 Vehicle 형의 변수는 위의 예제에서 본것처럼 모든 하위클래스의 객체도 참조할수있다. 위 예제를 보면 Vehicle 타입의 배열에도 SpecialVehicle 타입의 객체가 저장된다.

2017년 6월 27일 화요일

Object Oriented Programming(객체지향프로그래밍) 소개

객체지향프로그래밍(Object Oriented Programming), 줄여서 OOP는  전세대(1970년)의 절차지향프로그래밍(Procedural Programming)과 구조적프로그래밍(Structured Programming)을 갈아치우고 현세대를 지배하는 프로그래밍 패러다임이다. 대부분의 프로그래밍 언어들은 객체 지향적(Object Oriented)이므로 Java, C#, Python 등을 사용한다면 이 컨셉에 익숙해질 필요가 있다.

흔히 객체 지향적이라 말해지는 프로그램들은 객체(Object)들로 이루어진다. 각 객체들은 목적에 맞는 특별한 기능들을 가지고 있고, 사용자들은 그런 기능들을 사용할수 있다. 물론 객체 디자이너가 몇몇 기능들을 사용자들에게서 숨길수도 있다. 과거의 절차지향적, 구조적 프로그래밍들은 문제를 풀기위한 절차(알고리즘)을 설계하는것이였고, 그런 절차들이 결정되면 절차에 알맞게 자료를 저장할수있는 방법을 찾았다. 프로그래밍 언어인 Pascal을 개발한 Nikalus Writh는 "Algorithms + Data Structures = Programs" 라는 책을 1975년 발행하기도 했다. 책 이름을 보면 알고리즘이 앞에오고 자료구조가 뒤에오는데 이는 당시 프로그래밍 컨셉이 "알고리즘(절차) 설계가 데이터 관리보다 선행한다" 였다는 것을 알려준다. 하지만 OOP의 등장은 이 순서를 서로 뒤바꿔 놓아서, "데이터 관리가 알고리즘(절차)에 선행한다" 라는 패러다임을 만들었다.

간단한 프로그램의 경우 절차적 프로그래밍이 훨씬 쉽게 접근할 수 있다. 객체지향 프로그래밍은 더 큰 프로젝트에 적용된다. 간단하게 이 포스트를 보고있는 웹브라우져를 생각해보자. 일반 사용자가 사용하는 기능만 수백게에 달하며, 백그라운드에서 실행되는 기능들을 포함한다면 몇천개가 존재할텐데 만약 객체의 존재 없이 이 코드를 관리한다면 머리가 빠질거다. 객체지향 프로그래밍이 적용된다면 각기 목적에 맞는 클래스들과, 그 클래스들에 맞는 기능들이 잘 분류되기 때문에 훨씬 쉽게 관리가 가능하다.

그렇다면 클래스(Class)란 무었일까?


우리가 자바 프로그래밍에서 javac 라는 명령어로 .java 파일을 컴파일했을때 출력되는 그 클래스(Class)파일이다 클래스(Class)란 객체 생성을 위한 설계도와 같다. 클래스라는 설계도에 맞춰 객체가 생성(Construct)된다. 이때 생성된 객체를 클래스(Class)의 인스턴스(Instance)라 한다. 당연하게도 java파일에 적힌 모든 코드들이 class파일에 들어있다. Java API는 다양한 목적들을 위한 라이브러리를 제공하고, 라이브러리에는 Date, Math, Arrays등 수천개의 클래스들이 존재한다. 그리고 사용자는 본인만의 객체 생성을 위해 직접 클래스를 생성할 수 있다.

여기서 Encapsulation(캡슐화)이라는 컨셉이 등장하는데, 정보 은닉이라고도 알려진 이 컨셉은 말그대로 사용자에게서 객체가 저장하는 정보(Instance
Fields)들에대한 직접적인 접근을 막고, 메소드(method)를 통한 간접적인 접근만 가능케하여 실제 구현내용을 숨기는것이다. 그리고 객체의 현재 상태(State) 그런 Instance Fields들의 상태이며, method들이 호출하여 state를 변경할수있다. 이 캡슐화라는 속성은 클래스가 데이터를 관리하는법을 변경할 수는 있지만, 사용자 입장에서는 언제나 같은 메소드를 호출하므로 아무런 영향을 받지 않는다.

객체(Object)

앞서 말한 class가 인스턴스화된게 객체(Object)이다. 객체의 사용을 위해서는 3가지 특성을 파악해야한다.

  • 객체의 습성(Behavior) - 객체를 어떻게 활용하고, 어떤 메소드를 적용 가능한지
  • 객체의 상태(State) - 특정 메소드를 호출했을때 어떻게 상태가 변하는지
  • 객체의 정체성(Identity) - 동일한 동작과 상태를 가진 다른 객체와 어떻게 구별되는지

같은 클래스에서 인스턴스화된 객체들은 모두 같은 습성을 가지고 있다. 객체의 습성(Behavior)이란 사용자가 어떤 메소드를 호출할수 있느냐에 따라 결정된다. 그리고 각 객체는 데이터들을 저장하는데, 이게 객체의 상태(State)이다. 객체의 상태 변경은 메소드 호출의 결과여야 하고, 그 때문에 객체가 저장하는 데이터들이 모두 동시에 바뀌지는 않는다. 만약 두 객체의 State가 같다해도, 두 객체가 동일하다는것은 아니다. 이는 모든 객체의 정체성(Identity)이 서로 다르기 때문이다