Abstract Class와 Interface Class

추상클래스(Abstract Class)와 인터페이스(Interface) 대해서 알아보겠습니다.

추상화에 대한 이야기는 이미 객체지향언어의 특징에서 다뤘기 때문에 간단히 설명하고 넘어간 후 추상클래스를 어떻게 만드는지에 대해서 알아보겠습니다.

추상화는 객체에서 어떤 공통된 요소들(Attribute, Method)를 추출해서 클래스를 만드는 것이라고 했습니다. 그렇다면 추상화된 클래스는 일반 클래스와는 그 성격이 어떻게 다를까요?

일반 클래스는 new 라는 명령어를 통해서 객체를 만들어 낼 수 있지만 추상클래스는 new 라는 명령어를 통해서 객체를 만들어 낼 수 없고 상속이라는 과정을 통해서 객체를 구현해야 합니다. 자바에서는 이러한 클래스를 일반 클래스와 차별적으로 abstract class라고 표현합니다. 참고로 일반 클래스는 abstract를 붙이지 않습니다.

메서드도 마찬가지입니다. 일반 메서드는 구현체가 존재하지만 추상 메서드는 구현체가 존재하지 않고 이를 상속 받는 클래스에서 구현할 수 있도록 선언만 해줍니다. 이렇게 선언만 한 메서드를 추상메서드라고 표현하며 메서드의 앞에 abstract를 붙여줍니다.

클래스를 선언할 때에 하나라도 추상메서드가 포함된 클래스는 추상클래스가 됩니다.

그러다면 추상 클래스는 왜? 그리고 언제 사용하는 것일까요?

만약 게임 캐릭터를 만든다고 가정해봅니다.
캐릭터는 여러가지가 있겠죠. 기사, 궁수, 마법사, 힐러 등…

이러한 캐릭터에서 공통적인 요소들을 만들어 낼 수 있을까요?

캐릭터의 체력, 스피드, 키, 체중, 달리기, 걷기, 점프 등의 요소들을 공통적인 요소로 만들 수 있을 것입니다. 그리고 그 요소들을 Unit 이라는 추상클래스를 만들고 모든 게임 캐릭터는 해당 유닛 클래스를 상속하도록 만들 수 있을 것입니다.

public abstract class Unit {
	private String name;
	private int point_x;
	private int point_y;
	private int stamina;
	private int height;
	private int weight;
	
	
	public Unit(String name, int x, int y) {
		this.name = name;
		this.point_x = x;
		this.point_y = y;
	}
	
	public void move(int x, int y) {
		point_x += x;
		point_y += y;
	}
	
	public String getName() {
		return name;
	}
	
	public int getPointX() {
		return point_x;
	}
	
	public int getPointY() {
		return point_y;
	}
	
	public abstract void weapon();
	public abstract void jump();
	public abstract void run();
	
}

게임 프로젝트의 설계자는 이제 각 캐릭터 개발자에게 Unit 클래스를 상속해서 각기 캐릭터를 만들라고 지시 할 수 있을 것입니다. 그렇게 된다면 각 개발자들은 각 캐릭터마다 체력, 스피드, 키, 체중 등의 요소를 지정하고 달리고, 걷고, 점프 하는 동작들을 구현할 수 있을 것입니다.

이런 설계는 개발하면서 굉장히 중요한 부분입니다.
만약 이런 부분이 없이 각가의 개발자에게 캐릭터를 만들어 내라고 한다면 아마도 많은 혼란이 있을 것입니다. 만들어진 코드도 동일한 행위를 하는 다양한 이름으로 구현될 것입니다. 또 각각의 객체에 정의하는 초기값도 역시 프로그래머 각각 정의하게 될테니 개발하는 과정에서 많은 시간과 자원이 낭비될 것입니다.

이러한 것을 방지하기 위해서 추상클래스를 정의하고 이를 상속하도록 하는 것이 프로그램의 통일성을 주는데 용이합니다.

그렇다면 인터페이스는 언제 사용하는 것일까요?

인터페이스는 추상클래스와 비슷한 점이 많습니다. 다만 차이가 있다면 인터페이스는 공통적인 객체의 정의라기 보다는 행위에 집중합니다. 즉 구현체가 같은 행위를 한다는 것을 보장한다는 것입니다.
그렇기 때문에 추상클래스는 상속이라고 표현하지만 인터페이스는 행위의 구현이라고 표현합니다.

예를 들어서… 다른 언어를 한국어로 번역을 하는 프로그램을 만들어 본다고 생각해봅니다. 설계자는 번역의 행위에 집중할 것입니다. 다른 언어를 한국어로 번역하는 행위, 한국어를 다른 어로 번역하는 행위 이렇게 행위를 정의할 것입니다. 그리고 이 두행위를 하는 인터페이스 클래스를 만듭니다.

public interface TranslateInterface {
	public String translate2korean(String sentence);
	public String translate2foreignlanguage(String sentence);
}

이 인터페이스 클래스에는 두가지 역할을 하는 메서드 외에는 존재하지 않습니다. 그리고 이 파일을 다른 개발자들에게 나눠주고 이 파일을 통해서 인터페이스의 미구현 부분을 구현(implement)하라고 할 것입니다. 개발자들은 이 파일을 통해 각각 번역기를 구현할 때에 어떻게 구현해야 하는가에 대하여 고민할 필요가 없이 translate2korean, translate2foreignlanguage 메서드를 구현할 것이며 방식 역시 각가의 메서드에 source 문장을 주고 target 언어로 리턴하는 부분을 구현하면 됩니다.

지금까지 설명드린 것처럼 인터페이스와 추상클래스는 상속(혹은 구현)하는 과정을 통해서 프로그램을 더 명식적으로 만들어주고 협업시에 효율을 높여줍니다. 코드의 유지보수나 재사용에도 큰 장점이 있습니다.

객체지향 언어의 특징

캡슐화(Encapsulation)

보통의 사용자가 스마트폰을 사용하는데는 스마트폰의 구조와 작동원리에 대한 지식 보다는 사용에 필요한 기능이 무엇인지 또 어떤 때에 어떻게 사용하는지에 대해서만 알면 됩니다. 터치를 했을 경우에 소프트웨어가 어떻게 동작하고 하드웨어가 어떻게 동작하는지에 대해서 몰라도 사용하는데는 지장이 없습니다.

만약 기기가 전부 오픈되어 있어서 사용자가 조작하지 말아야 할 부분까지 만질 수 있도록 되어 있다면 친절하다기 보다는 사용자의 실수나 기기의 고장의 원인이 될 것입니다.

프로그램도 마찬가지로 사용자가 내부의 주요 소스코드에 접근하는 것은 오히려 좋지 않습니다. 많은 변수들이 내부에서 생성되고 소멸되는데 그런 민감한 변수들을 조작하는 것은 프로그램 오류의 원인이 됩니다.

개발자는 이러한 것을 미연에 방지하도록 주요 로직과 변수들을 숨기고 오직 사용자가 최소한의 프로그램 동작에 필요한 것들만 접근하고 사용할 수 있도록 하는 것이 좋습니다.

이러한 것을 객체지향의 캡슐화 혹은 정보은닉이라고 표현합니다.

추상화(Abstraction)

추상화는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념이나 기능을 간추려 내는 것이라고 할 수 있습니다.

예를 들어 여러 브랜드의 다양한 자동차가 있습니다. 모든 자동차들은 그 종류에 따라서 크기, 무게, 넓이, 높이 등의 차이가 있습니다. 그러나 이러한 차이에도 불구하고 모든 자동차들을 잘 관찰하면 몇가지 공통된 특징을 찾아 낼 수 있습니다.

스티어링 휠, 바퀴, 주행장치, 제동장치, 문, 헤드라이트 등 공통점을 찾아내서 하나의 기본 개념을 만듭니다. 이렇게 만들어진 객체를 추상화된 객체라고 할 수 있습니다. 추상화된 객체는 “IS A”관계로 나타낼 수 있습니다. 즉, “기아자동차의 K7은 자동차이다”라는 관계를 만들어 낼 수 있습니다. 이와 마찬가지로 “현대자동차의 그랜저는 자동차이다”라는 관계가 맺어집니다. 이렇게 하면 자동차로부터 추상화된 객체는 “IS A” 관계가 됩니다.
그러나 “자동차는 K7이다.” 관계는 좀 어색합니다.

이 개념은 간단하지만 추상화의 매우 중요한 개념입니다.

같은 개념을 이전 프로그램 코드로 본다면 사람의 모든 특징을 조합하여 Human이라는 추상클래스를 만들었다고 생각해 봅니다. 그리고 Man 이라는 인터페이스 클래스를 만듭니다. 이제 Human + Man = Adam 이라는 클래스를 만들었습니다.

이 Adam이라는 객체를 통해서 “David”, “John” 객체를 만듭니다.
이렇게 하면 David ≠ John 이지만 Human = David, Human = John의 관계는 성립하게 됩니다.

public class HumanDemo {
	public static void main(String[] args) {
		Adam david = new Adam("David", 180, 80);		
		Human john = new Adam("John", 178, 75);
		Man mark = new Adam("Mark", 178, 75);
	}
}

이렇게 추상화한 객체와 이를 상속 받은 객체 사이에 “IS A”의 관계가 성립한다는 것이 추상화의 중요한 개념입니다.

상속(Inheritance)

상속은 추상화된 클래스 즉, 객체의 일반적인 특성이 정의된 클래스를 사용하여 새로운 객체를 만드는 것을 의미합니다. 단 추상화된 클래스는 반드시 추상클래스라는 의미는 아니니 혼동 없으시기 바랍니다.

이는 보통 부모-자식 클래스 혹은 상위-하위 클래스로도 이름할 수 있습니다. 앞으로는 자식 클래스로 이름하겠습니다. 부모 클래스는 super를 통해서 접근이 가능합니다.

자식클래스는 부모 클래스의 속성을 물려 받을 수 있습니다. 별도로 정의하지 않아도 되지만 필요하다면 정의할 수도 있는데 이것을 Overriding이라고 합니다.

이러한 상속의 개념을 이해하고 설계에 반영하면 객체의 재사용이나 기능 추가 혹은 유지보수 등에 큰 장점이 있습니다.

자바(JAVA)에서 상속은 extends를 통해서 가능하고 extends를 통해서는 단일 상속 밖에는 지원하지 않습니다. 그러나 interface class의 경우 implements를 통해서 다중 상속도 가능합니다.

다형성(Polymorphism)

객체지향의 특징에서 가장 이해하기 어려운 개념이 바로 다형성 개념일 것입니다. 다형성은 말 그대로 상속 받은 메서드가 자식 클래스에서 다양한 방식으로 개발 할 수 있도록 허용하는 것이다. 이 다형성이 상속과 연계되어서 자바의 높은 언어적인 효율성이 발휘된다고 할 수 있습니다.

이 다형성을 잘 이해하고 설계하면 코드도 간결해질 뿐더러 변화에도 유연하게 대처할 수 있습니다.

아래의 코드는 일반적인 형태의 코드입니다. 한국인, 중국인, 영국인 객체를 선언하고 각각 모국어를 말하는 클래스입니다. 이 세 객체는 “말한다”라는 하나의 기능을 수행하는 메소드를 가지고 있지만 각기 다른 이름으로 구현되어 있습니다.

package ch01;

public class SpeakDemo {
	public static void main(String[] args) {
		Korean k = new Korean();
		Chinese c = new Chinese();
		Briton b = new Briton();
		
		k.speakKorean();
		c.speakChinese();
		b.speakEnglish();
	}
}

class Korean {
	public void speakKorean() {
		System.out.println("한국어");
	}
}

class Chinese {
	public void speakChinese() {
		System.out.println("중국어");
	}
}

class Briton{
	public void speakEnglish() {
		System.out.println("영어");
	}
}

이제 위의 코드에서 Korean, Chinese, Brion 클래스에서 공통적인 요소를 추출하여 추상클래스를 만들고 이 세 언어의 공통적인 기능인 “speak()” 메소드를 정의하겠습니다. 이 추상클래스를 공통으로 상속 받는 객체들은 반드시 speak() 메소드를 구현해야 합니다.
그러나 이 speak() 메소드를 상속 받은 클래스는 각자의 상황에 맞춰서 해당 메소드를 다르게 구현합니다. 추상클래스에서는 정의만 하고 이를 상속하는 클래스에서 각기 다른 방식으로 내용을 구성하는 것입니다.

package ch01;

public class SpeakDemo2 {
	public static void main(String[] args) {
		Language ko = new Korean();
		Language ch = new Chinese();
		Language br = new Briton();
		
		ko.speak(); ch.speak(); br.speak();
	}
}

abstract class Language {
	public abstract void speak();
}

class Korean extends Language {
	@Override
	public void speak() {
		System.out.println("한국어");
	}
}

class Chinese extends Language {
	@Override
	public void speak() {
		System.out.println("중국어");
	}
}

class Briton extends Language { 
	@Override
	public void speak() {
		System.out.println("영어");
	}
}

위와 같은 방식으로 코딩하면 세 객체간의 관계가 명료해지고 또 다른 언어가 생성 될 때에 상속을 받고 speak() 부분만 구현하면 되기 때문에 코드가 간결해지고 재활용이 가능해집니다. 그리고 부모 객체의 메소드를 자식에서 구현할 때에 명시적으로 @Override를 어노테이션을 표기해주면 더욱 명시적입니다.

비슷한 개념으로 @Overload가 있습니다.
해당 개념은 하나의 메소드 이름을 각기 다른 매개 변수를 사용해서 호출하는 것으로 가장 많이 쓰는 예로는 System.out.print() 함수가 있습니다. 해당 함수는 String, boolean, long, int 등 매개변수를 다르게 해도 모두 동일한 print()를 사용합니다.

그러므로 오버로딩의 조건은 메서드의 이름이 같고, 매개 변수의 타입이 틀려야 합니다. 만약 Overload가 되지 않았다면 아마도 자바는 비슷한 이름의 지금 보다 훨씬 더 많은 메소드를 보유하면서 개발자를 힘들게 만들었을 수도 있을 것입니다.

객체지향의 역사와 이해

초기의 컴퓨터가 나타났을 때에 연산은 주로 배선으로 만들어 졌습니다. 이런 방식은 알고리즘의 수정이나 변경이 어려웠기 때문에 당연히 많은 문제점이 나타났고 이에 대한 개선의 노력이 이어졌습니다. 그러다가 폰 노이만(Von Neumann architecture)에 의해서 현대적인 컴퓨터 구조가 제안되면서 하드웨어, 소프트웨어로 구분되기 시작했습니다. 이후 1950년 부터 프로그램 언어가 등장하기 시작했습니다.

wikipedia.org

이후 연구자들은 소프트웨어 공학이라는 소프트웨어의 개발, 운용, 유지보수의 생명 주기 전반을 체계적으로 다르는 학물을 발전시켰습니다.
최초의 소프트웨어 공학용어를 사용한 해는 1968년으로 알려져 있습니다. 이 당시는 소프트웨어의 개발 속도가 하드웨어 개발 속도를 따라가지 못해 사용자들의 요구사항을 처리 할 수 없는 문제들이 발생되는 소프트웨어 위기(Software Crisis) 론이 등장하는 시기였습니다.
이러한 시대적인 요구사항 속에서 연구자들은 자연스럽게 이 위기를 어떻게 극복할 것인가에 대하여 그 해결책을 찾기 위해 고민했습니다.

객체지향언어도 이 당시에 최초로 등장했습니다.
최초의 객체지향 언어는 시뮬라67이었습니다. 비록 이 당시에 큰 주목을 받지 못했지만 향후 객체지향언어의 발전의 큰 영향을 주었습니다. 이후 스몰토크, 에이다 같은 프로그램들이 등장하면서 객체지향에 대한 연구가 활발하게 이뤄졌습니다.

객체지향에 대한 필요는 기존 언어의 한계에서 비롯되었다고 할 수 있습니다. 기존에는 절차식 프로그래밍(Procedural Programming)은 하위 프로그램, 서브 루틴, 메서드, 함수 등의 용어로 활용되는 프로시저를 호출하며 프로그램을 실행하는 것이었습니다.
어려운 말로 들리지만 간단히 말하면 수행되어야 할 코드를 연속적으로 정의하는 것이라고 할 수 있다.

비교적 프로그램의 구조가 간단하기 때문에 많이 사용되었지만 대규모 개발에서 생산성이 좋지 않고 프로그램의 유지보수가 좋지 않았기 때문에 여러가지 많은 문제들이 발생했습니다.

객체지향은 이러한 환경에서 출발했기 때문에 자연스럽게 프로그램 코드의 재사용성을 높여 생산성을 향상 할 수 있을까 또 어떻게 하면 프로그램의 유지보수나 기능 개선에 드는 시간과 비용을 절약할 수 있을까라는 문제 해결에 주안점을 두게 되었습니다.

그 결과 클래스(Class), 객체(Object), 메서드(Method) 를 기본 구성요소로 추상화, 상속, 다형성 등을 특징으로 가진 객체지향 언어가 등장했고 강한 응집력(Strong Cohesion)과 약한 결합력(Weak Coupling)을 통해서 기존 언어의 문제점들을 극복하고 1995년 자바가 등장한 이후 가장 강력한 언어가 되었습니다.

객체지향언어는 1905년 이후 자바가 등장하면서 많은 대중화를 이뤄냈고 자바(JAVA)는 객체지향이라는 등식을 만들어 냈습니다.

그렇다면 객체지향이란 무엇인가?
이 말은 객체와 지향이라는 두 단어의 조합으로 이뤄져있는 단어입니다.

객체(Object)는 이 세상의 존재하는 모든 것들이라고 할 수 있습니다. 그리고 지향한다는 것은 방향성을 의미한고 할 수 있습니다. 즉 객체지향이라는 것은 세상에 존재하는 객체가 어떻게 동작하고 구성되어 있는가의 원리를 프로그램으로 도입한 것이라고 할 수 있겠습니다.
그러나 객체지향은 여전히 절차적 언어의 특징을 가지고 있으며 기존의 다양한 제어문, 반복문 등을 사용합니다. 다만 기존의 개발방식에 객체라는 지향점을 코딩의 패러다임을 변화한 것이라고 생각하시면 됩니다.

예를 들어 인간이라는 캐릭터를 컴퓨터 상에 구현한다고 생각해봅니다.
(* 이것은 어떤 철학이나 의학 등의 개념이 아닌 프로그램의 예로 든것입니다.)

인간 = Attribute + Method 로 정의할 수 있습니다.
Attribute는 인간을 표현하는 여러가지 속성들 즉, 눈, 코, 입, 귀, 다리, 팔, 키, 몸무게 등이 있고 Method는 프로그램 안에서의 서다, 걷다, 눕다 등 인간의 동작이나 행위를 규정한다고 할 수 있습니다.
단, 프로그램 안에 모든 속성을 다 객체에 담을 필요는 없고 만들고자 하는 프로그램에서 목적하는 몇가지 속성과 메서드를 정의하면 됩니다. 본 예제에서는 이름과, 키, 몸무게 정도만 사용해서 human이라는 추상클래스(Attribute + Method)로 만들었습니다.

모든 인간 캐릭터는 추상클래스를 상속하게 됩니다. 그렇기 때문에 추상클래스는 이를 상속하는 객체의 대표성을 띄게 됩니다.

package ch01;

public abstract class Human {

	private String name;
	private int height;
	private int weight;
	
	public Human(String name, int height, int weight) {
		this.name = name;
		this.height = height;
		this.weight = weight;
	}
	
	public abstract void run();
	public abstract void jump();
	public abstract void walk();
	
	public void getFeature() {
		System.out.println(String.format("%s, %d, %d", name, height, weight));
	}
}

사람은 남자와 여자로 나눌 수 있기 때문에 인간의 특징 중에 남자의 특징적 요소를 다음과 같이 정의했습니다.

package ch01;

public interface Man {
	public void work();
	public void fight();
}

만약 어떤 클래스가 Human 클래스와 Man 인터페이스를 상속 받는 다면 해당 클래스는 이름, 키, 몸무게, 달리고, 뛰고, 걷고, 싸우고, 일하는 속성을 공통으로 받게 됩니다. 이것을 상속이라고 합니다.

예제는 Adam이라는 클래스를 만들고 Human, Man 등의 클래스를 상속 받았습니다.

package ch01;

public class Adam extends Human implements Man {

	public Adam(String name, int height, int weight) {
		super(name, height, weight);
	}

	@Override
	public void work() { }

	@Override
	public void fight() { }

	@Override
	public void run() { }

	@Override
	public void jump() { }

	@Override
	public void walk() { }
}

이제 상속 받은 클래스를 통해서 하나의 객체를 만들었습니다. 사실 클래스는 어떤 객체를 만드는 설계도라고 할 수 있고 이 설계도를 통해서 컴퓨러 메모리 공간에 만들어 낸 것을 객체(Object)라고 합니다. (이 이름에는 약간씩 다른 의미로 부르기도 합니다.)

package ch01;

public class HumanDemo {
	public static void main(String[] args) {
		Adam adam = new Adam("아담", 180, 80);
		Eve eve = new Eve("이브", 165, 50);
		
		adam.getFeature();
		eve.getFeature();
	}
}

이런 식으로 코딩하게 되면 인간+남자 클래스를 상속 받아 각기 다른 수많은 남자 객체를 메모리 상에 구현할 수 있습니다. 마찬가지로 인간+여자 클래스를 상속 받으면 수많은 여자 객체를 메모리 상에 구현 할 수 있습니다.

이러한 프로그램 개발 방식이 객체지향입니다. 이와 같은 방법으로 Car 클래스를 정의하고 이를 상속 받아 Bus, Taxi 등의 객체를 만들 수 있고 Animal 클래스를 정의하고 이를 상속 받아서 Cat, Dog 등의 클래스를 만들어 낼 수도 있습니다.
이렇게 개발하면 코드의 재사용성을 높일 수 있고 기존의 기능을 수정하거나 새로운 기능을 추가하거나 하기가 용이합니다.
그러나 그만큼 객체지향적 설계에 많은 시간을 들여야 합니다.

객체지향 언어는 몇가지 중요한 특징을 가지고 있습니다.

추상화(Abstraction), 캡슐화(Encapsulation), 상속(Inheritance), 다형성(Polymorphism)이 그것입니다.

다음에는 이러한 요소들에 대해서 살펴보도록 하겠습니다.