티스토리 뷰
클래스 디자인(Class design)이라는 것은 한 프로그램의 클레스를 설계하는 것이다. 어떤 건물을 지을 때 건물의 설계도를 그리고 그 설계도에 따라서 건물을 짓는다. 그리고 그 설계도라는 것은 규모가 커질수록 중요도가 높아진다.
소프트웨어도 마찬가지이다. 만약 소규모 프로젝트를 진행함에 있어서 설계도는 간단하게 넘어갈 수도 있다. 하지만 프로젝트의 규모가 커질수록 그 설계도는 정교하게 작성되어야 할 것이다.
먼저 클래스, 추상 클래스, 인터페이스의 개념을 잡고 넘어가도록 하자.
클래스(Class)
클래스는 하나의 설계도라고 보면 될 것이다. 클래스에는 멤버변수, 메소드, 내부 클래스 등이 존재할 수 있다. 이 클래스라는 설계도를 통해 실체화한 것이 객체(Instance)이다. 또는 인스턴스라고 말하기도 한다.
// 클래스
class Persons {
int age; // 멤버변수
String name; // 멤버변수
// 메소드
void eat() {
System.out.println("냠냠");
}
}
위 코드를 보면 클래스와 클래스 내부의 멤버변수와 메소드가 존재하는 구조를 한눈에 알아볼 수 있다. 우리가 객체화할 수 있는 것은 클래스이다.
추상 클래스(Abstract Class)
클래스에 추상 메소드(Abstract Method)가 하나라도 존재한다면 이 클래스는 추상 클래스로 선언해 주어야 한다. 추상 메소드가 존재한다는 점을 제외하고는 클래스와 모든 것이 동일하다. 이는 클래스 선언부 앞에 abstract 제어자를 붙여서 선언한다. 마찬가지로 메소드 선언부 앞에 abstract 제어자를 붙여서 선언하면 이 메소드는 추상 메소드가 된다.
추상 메소드의 특징은 구현된 부분이 존재하지 않는다는 것이다. 단지 선언부(메소드이름, 리턴타입, 매개변수)만을 정의하는 메소드이다. 정확한 형식은 아래와 같다.
abstract 리턴타입 메소드이름(매개변수);
따라서 이는 미완성 설계도 정도로 이해하면 쉬울 것이다. 이 특징은 곧 직접 객체로 생성할 수 없다는 결론을 도출한다. 왜나하면 구현되지 않은 추상 메소드가 존재하기 때문이다.
이 추상 메소드라는 것은 다른 클래스에서 상속받아 반드시 구현해 주어야 한다. 구현하지 않는다면 추상 클래스를 상속받은 자식 클래스 역시 추상 클래스로 선언해 주어야 한다.
// 추상 클래스
abstract class Person {
int age; // 멤버변수
String name; // 멤버변수
// 메소드
void eat() {
System.out.println("냠냠");
}
// 추상 메소드
abstract void work();
}
// 클래스
class Teacher extends Person {
// 추상 메소드 구현
@Override
void work() {
System.out.println("가르친다.");
}
}
// 클래스
class Actor extends Person {
// 추상 메소드 구현
@Override
void work() {
System.out.println("연기한다.");
}
}
Person 클래스에는 이름, 나이를 나타내는 멤버변수가 존재하고, 구현되어 있는 eat 메소드, 그리고 구현체가 없는 work 메소드가 존재한다. 그리고 이 Person 클래스를 상속받는 Teacher 클래스와 Actor 클래스가 존재한다. 각 클래스는 Person 클래스의 work 추상 메소드를 구현하고 있는 모습이다.
여기서 추상 클래스의 장점은 자식 클래스에서 반드시 특징적으로 구현해야 하는 부분을 추상 클래스로 정의하고, 공통된 역할을 하는 부분은 부모 클래스에서 구현을 해놓은 상태로 상속을 해줄 수 있기 때문이다.
추상 클래스에는 생성자가 필요할까?
결론부터 얘기하자면 필요하다. 추상 클래스는 객체로 생성할 수 없으니 생성자가 필요하지 않다고 오해할 수 있지만, 부모 클래스를 상속받은 자식 클래스를 객체로 생성하게 되면 자동으로 부모의 생성자는 실행되게 된다. 왜냐하면 부모 클래스의 멤버변수 또한 자식 클래스에서 접근이 가능해야 하기 때문에 당연히 부모 클래스의 생성자가 실행이 되어야하는 것이다.
여기서 우리가 super 생성자를 사용하는 이유는 부모의 생성자가 오버로딩되어 있는 상태라면 부모 생성자 중 필요한 생성자를 불러줄 필요가 생긴다. 이럴때는 super 생성자를 명시적으로 호출해 주어야 한다. 하지만 우리가 super 생성자를 호출하지 않아도 자동으로 디폴트 생성자는 호출이 된다는 것이다.
인터페이스(Interface)
인터페이스는 추상 클래스를 가장 추상적으로(?) 구현한 추상 클래스의 한 종류라고 할 수 있다. 인터페이스에는 추상 메소드(Abstract Method)와 상수(Constant)만 존재할 수 있다. 따라서 추상 클래스와 달리 실제적인 구현체가 존재할 수 없다. 여기서 구현체라는 것은 상수를 제외한 멤버변수와 구현체가 있는 메소드를 뜻한다.
인터페이스의 구체적인 문법은 다음과 같다.
interface 인터페이스이름 {
public static final 상수이름 = 상수값;
public abstract 리턴타입 메소드이름(매개변수);
}
기본적인 문법은 다음과 같지만, 저 문법대로 쓰지 않아도 컴파일러는 알아서 메소드에는 public abstract 제어자를 붙여주고 멤버변수에는 public static final 제어자를 넣어준다.
인터페이스에는 실체적인 구현체가 없기 때문에 당연히 객체화하는 것은 불가능하다. 따라서 인터페이스를 사용하려면 다른 인터페이스에서 상속(extends)하거나, 추상 클래스, 클래스에서 구현화(implements)를 해줘야 한다.
// 인터페이스
interface Actions {
int MAX_AGE = 150; // 상수
public final static int BIRTH = 0; // 상수
void eat(); // 추상 메소드
public abstract void aging(); // 추상 메소드
abstract void work(); // 추상 메소드
}
// 추상 클래스
abstract class Person implements Actions {
int age; // 멤버변수
String name; // 멤버변수
Person() {
age = 0;
}
// 메소드
@Override
public void eat() {
System.out.println("냠냠");
}
// 메소드
@Override
public void aging() {
System.out.println("나이를 먹는다.");
}
}
// 클래스
class Teacher extends Person {
// 추상 메소드 구현
@Override
public void work() {
System.out.println("가르친다.");
}
}
// 클래스
class Actor extends Person {
// 추상 메소드 구현
@Override
public void work() {
System.out.println("연기한다.");
}
}
위의 추상 클래스 예제에서 인터페이스 기능을 도입하였다. 인터페이스는 동작 정보만을 담은 Actions 인터페이스로 따로 분류해 보았다. 동작 정보만을 담은 추상 메소드를 Person 추상 클래스에서 상속받아 일부는 구현해주고 Teacher 클래스와, Actor 클래스에서 특성화할 부분은 각 클래스에서 구현화하는 모습을 볼 수 있다. 아래 그림으로 이해를 도울 수 있을 것이다.
인터페이스의 특징
인터페이스에서 상수나 메소드에 제어자를 생략해줘도 정상적으로 컴파일이 가능한 것으로 보아, 컴파일러가 자동적으로 제어자를 삽입하여 컴파일한다는 것을 알 수 있다.
인터페이스는 다중 상속을 허용한다. 사실 상속이라는 개념보다 구현화(implement)라는 개념을 따로 생성한 것도 그런 이유이다. 우리가 C++에서는 다중 상속을 허용하는 것을 볼 수 있다. 하지만 다중 상속이라는 것은 어떻게 보면 클래스의 관계를 복잡하게 만드는 원인이기도 하다.
그래서 자바에서는 클래스끼리의 다중 상속을 허용하지 않으면서, 필요에 의해서 인터페이스로 여러 기능을 구현하는 형태로 이를 허용하게 된다.
클래스 설계를 하는 이유
여기서 의문점이 생길 수 있다. 뭐하러 귀찮게 빈 껍데기만 만들어서 그걸 가지고 구현화를 하는 과정을 거치는 것이냐고 반문할 수 있다. 솔직히 말하면 클래스로 설계를 해도 상관없다. 인터페이스의 다중 구현화 기능을 제외하면, 전부 동일하게 구현할 수 있다. 그런데 굳이 인터페이스, 가상 클래스를 사용하는 이유는 무엇인가?
1. 표준화에 용이하다.
우리가 모든 프로젝트를 혼자서만 진행할 수 있을까? 절대로 그렇지 않다. 하나의 프로그램을 여러 사람이 코딩하는 경우가 무수히 많다. 하지만 이들을 코딩하는데 있어서 메소드의 이름을 동일하게 맞춰줘야할 경우도 있다.
예를들어서 어떤 사람이 게임 캐릭터를 움직이는 메소드를 구현하는데 어떤 사람은 그 메소드의 이름을 walk으로 정하고 어떤 사람은 move로 정하면 그 프로그램은 같은 움직이는 동작을 하는 캐릭터들이 다른 메소드명을 가지게 된다. 이름만 다르면 다행이다. 그 메소드의 매개변수가 다르다면 이는 더 골치아파진다.
따라서 이러한 개발의 협업에 있어서 하나의 룰을 프로그래밍적으로 구현하는 기능을 한다.
2. 복잡한 프로그램의 구조를 계층화 한다.
프로젝트가 커질수록 프로그램은 복잡한 구조를 가지게 된다. 이런 상황에서 모든 프로그램을 단지 클래스로만 설계한다면, 후에 자신의 클래스가 어떻게 설계되어 있는지 확인할 때 상당히 어려운 작업이 될 것이다.
따라서 클래스의 관련있는 일부 기능을 그룹화하여 인터페이스로 적용시키고, 그 인터페이스를 구현화하는 방법을 사용한다. 이런 방법을 사용하게 되면 인터페이스만 보고도 이 프로그램의 구조를 대략적으로 파악할 수 있게 된다. 그리고 이 인터페이스를 구현한 모든 클래스들도 함께 관리가 가능해지는 것이다.
인터페이스 vs 추상 클래스
그렇다면 설계를 할 때 어떤 방식을 사용하는 것이 바람직할까? 사실 이 답에 정답은 없다. 왜냐하면 인터페이스와 추상 클래스를 사용하여 설계하는 것들은 그 프로그래머의 취향에 많은 영향을 받게된다.
하지만 개발자들이 수많은 개발을 진행하면서 잘 사용된 예시를 보면 대체적인 경향을 찾아볼 수 있다.
1. 인터페이스는 초상위 단계에서 사용된다.
2. 추상 클래스는 인터페이스를 상속받아 일부 공통된 메소드를 구현화할 때 사용한다. 중, 하위 단계에서 사용된다.
3. 클래스는 추상 클래스 또는 인터페이스를 상속받아서 구현되고, 추상 클래스에서 구현되지 않은 메소드를 구현하여 특성화한다.
4. 구현된 클래스는 세부 특징별로 자식 클래스로 상속받아서 구현한다.
참고
Java의 정석 - 남궁성
http://history1994.tistory.com/12
https://opentutorials.org/course/1223/6063
'Programming > Java' 카테고리의 다른 글
[Java]접근 제어자(Access Modifier) (0) | 2018.07.12 |
---|---|
[Java]예외(Exception) (0) | 2018.07.11 |
[Java]상속(Inheritance) (0) | 2018.07.04 |
[Java]환경변수(JAVA_HOME & CLASSPATH) (5) | 2018.07.04 |
[Java]Call by value & Call by reference (1) | 2018.07.02 |