코린이의 소소한 공부노트

추상 클래스 작성하기 본문

Java

추상 클래스 작성하기

무지맘 2022. 5. 11. 13:29

추상 클래스를 만드는 방법

1. 여러 클래스에 공통적으로 사용될 수 있는 메서드, 변수를 이용해 추상 클래스를 바로 작성하거나

2. 기존 클래스의 공통부분을 뽑아 추상 클래스를 만든다.

 

예시를 보는 게 가장 빨리 이해가 될 것이다.

class Infantry { // 보병
    int x, y;
    void move(int x, int y) { }
    void stop() { }
    void stimPack() { }
}

class Tank{ // 탱크
    int x, y;
    void move(int x, int y) { }
    void stop() { }
    void changeMode() { }
}

class Dropship{ // 수송선
    int x, y;
    void move(int x, int y) { }
    void stop() { }
    void load()   { }
    void unload() { }
}

군대 유닛 중 대표적인 것 3가지만 클래스로 만들어봤다.

1) 공통부분

  - x, y: 좌표를 나타내는 멤버 변수

  - move(): (x, y)로 이동시키는 메서드. 보병은 걸어서, 탱크는 바퀴를 굴려서, 수송선은 물 위에 떠서 이동.

  - stop(): 현재 위치에 정지시키는 메서드

2) 각 클래스에만 있는 부분

  - stimpack(): 보병 클래스, 스팀팩 사용

  - changeMode(): 탱크 클래스, 공격 모드 변환. 이동하면서 공격 또는 서서 공격

  - load(), unload(): 수송선 클래스, 대상을 싣거나 내림

이 중 공통부분을 추상 클래스로 작성한 후 이를 상속받게 바꿔보겠다.

abstract class Unit {
	int x, y;
	abstract void move(int x, int y);
	void stop() { }
}

class Infantry extends Unit {
	void move(int x, int y) { }
	void stimPack() { }
}

class Tank extends Unit {
	void move(int x, int y) { }
	void changeMode() { }
}

class Dropship extends Unit {
	void move(int x, int y) { }
	void load()   { }
	void unload() { }
}

  - x와 y, stop()은 똑같이 사용되기 때문에 그대로 뽑아냄

  - 구현부는 다른데 선언부는 같은 move의 경우 추상 메서드로 만듦

공통부분을 뽑아냈기 때문에 코드가 간결해졌다. 이제 메인에서 한번 사용해보자.

Unit[] group = { new Infantry(), new Tank(), new Dropship() };
// 위 코드는 아래와 같다.
// Unit[] group = new Unit[3];
// group[0] = new Infantry();
// group[2] = new Tank();
// group[3] = new Dropship();

for (int i = 0; i < group.length; i++)
    group[i].move(100, 200);

Unit 타입의 배열에 자손 타입인 보병, 탱크, 수송선 객체를 하나씩 만들었다. 그리고 for문을 통해 move()를 호출했다.

  - i=0: Infantry 클래스의 move() 호출

  - i=1: Tank 클래스의 move() 호출

  - i=2: Dropship 클래스의 move() 호출

각 객체의 move()가 실행된다.

위와 같은 코드가 가능한 이유는

 1) 다형성이 적용되기 때문이다.

  - 각 클래스가 Unit 클래스의 자손 클래스이기 때문에 Unit 타입의 객체 배열에 각 자손 클래스의 객체를 담을 수 있다.

 2) move()를 추상 메서드로 작성한 후 각 자손 클래스에서 구현했기 때문이다.

  - 참조변수가 Unit 타입이기 때문에, Unit 클래스의 멤버여야 접근이 가능하다.

  - 참조변수 group이 접근 가능한 멤버는 x, y, move(), stop()이다.

  - 만약 Unit 클래스에 move()가 없다면 for문에서 에러가 발생한다. 접근할 수 없기 때문이다.

  - 위 배열 선언문에서 Unit[]이 아니라 Object[]였다면 에러가 발생한다. Object 클래스에는 move()라는 메서드가 없기 때문이다.

 

[추상 클래스 작성 시 장점]

1. 자손 클래스를 쉽게 작성할 수 있다.

 1) 추상 클래스를 작성해 놓으면, 이후 비슷한 기능을 하는 새 자손 클래스도 쉽게 만들 수 있다.

 2) 위 예제에서 제트기를 추가한다고 하면, Unit 클래스를 상속받아 아래와 같이 쓸 수 있을 것이다.

class Jet extends Unit{
    void move(int x, int y) { }
    void attack()
}

2. 중복이 제거된다.

 1) 공통부분을 뽑아서 추상 클래스를 만들었기 때문에, 자손 클래스에서 중복이 제거된다.

 2) 코드가 간결해지고 보기 편해진다.

3. 변경 시 관리가 용이하다.

 1) 추상 클래스에 있는 내용을 바꾸면 자동으로 자손 클래스에도 적용이 된다.

 2) 추상 클래스를 상속받은 것이 아닌, 별개의 클래스로 구현이 되어있었다면 고쳐야 할 부분이 많아진다.

    예를 들어 위 예시에서 stop()를 고친다고 할 경우

    - 추상 클래스를 상속받았을 때는 Unit 클래스만 수정하면 되는데

    - 개별 클래스였을 경우 클래스마다 stop()을 찾아서 수정해야 한다.

 3) 추상 클래스를 단계적으로 나눠서 상속받으면 추가도 쉽다.

A, B, C 순서로 추상 클래스를 단계적으로 만들었다고 할 때

  - A를 상속받은 또 다른 클래스 B', C'로 이어지는 단계를 새로 만들 수도 있고,

  - B를 상속받은 또 다른 클래스 C''를 만들 수도 있다.

필요한 단계에서 상속받아 추상 메서드를 구현함으로써 코드 중복도 줄이고 보다 간편하게 클래스를 추가할 수 있다.

 

추상화된 코드를 쓰는 가장 큰 이유는 구체화된 코드보다 유연하기 때문이다. 유연하다는 것은 코드 변경이 생겼을 때 대처를 잘한다는 뜻이다. 불명확한 추상화가 명확한 구체화보다 왜 유리한 위치를 가져가는지 예시를 보겠다.

달력을 2가지를 만들어보려고 한다. 하나는 특정 달력을 만들고, 다른 하나는 코드만 봤을 때 어떤 달력인지 모르는 것을 만들 것이다.

GregorianCalendar c1 = new GregorianCalendar(); // 구체적. 우리나라는 서양력을 사용함
Calendar c2 = Calendar.getInstance(); // 추상적. 다형성

  - c1의 경우 객체의 타입과 참조변수의 타입이 일치한다. 어떤 객체인지 분명하고 명확하게 나타나 있다.

  - c2의 경우 참조변수는 Calendar라는 추상 클래스 타입이고, 객체는 Calendar 클래스의 자손 객체이다.

이렇게 보면 타입이 명확하게 나타나 있는 c1을 사용하는 것이 더 좋아 보인다. 하지만 c2를 사용하는 것이 훨씬 좋을 때가 있다. 아래 코드를 살펴보자. 

public static Calendar getInstance(Locale aLocale){
    return createCalendar(Timezone.getDefault(), aLocale);
}

private static Calendar createCalendar(Timezone zone, aLocale){
    // 중략
    if(caltype != null){
        switch(caltype){
            case "buddhist": // 불교력
                cal = new BuddhistCalendar(zone, aLocale);
                break;
            case "Japanese": // 일본력
                cal = new JapaneseCalendar(zone, aLocale);
                break;
            case "gregory": // 서양력
                cal = new gregoryCalendar(zone, aLocale);
                break;
    // 이하 생략
}

  - getInstance() 메서드는 자바를 돌리는 컴퓨터에 있는 정보를 보고 어느 국가인지, 시간대는 표준시간대에서 얼마나 차이가 나는지 확인한 후 createCalendar()를 이용해 객체를 생성 후 반환한다.

  - createCalendar() 메서드는 해당 지역(국가)이 불교국가(buddhist)면 불교력을, 일본(Japanese)이면 일본력을, 서양력(gergory)을 쓰면 서양력(그레고리력)을 만들어서 반환한다. 

 

아직도 왜 추상화가 더 좋은지 모르겠다면, 다음 예시도 한번 읽어보자.

어떤 프로그램을 만들었다고 가정해보자.

  1) 처음에는 우리나라를 타깃으로 만들어서 c1처럼 구체적으로 객체의 타입과 참조변수의 타입을 정해놨었는데, 시간이 지나면서 이 프로그램이 전 세계적으로 인기를 끌게 되었다. 그와 거의 비슷한 시기에 세계 각국의 서양력을 쓰지 않는 나라들의 유저들이 자기 나라와 맞지 않다며 불만을 토로하기 시작했다. 그래서 대대적으로 수정에 들어갔는데, 처음부터 서양력에 맞춰 코딩을 했다 보니 수정할 곳이 한두 군데가 아니다. 한 군데 고치면 다른 데서 버그, 겨우겨우 버그 잡았다고 좋아했는데 또 다른 데서 삐걱삐걱.. 매우 끔찍한 상황이다.

  2) 이 프로그램이 전 세계적으로 인기를 끌 것이라 예상하고 c2처럼 달력을 만들어 코딩하였다. 처음에는 우리나라에서만 인기가 좋았으나, 점점 입소문을 타서 예상대로 전 세계적으로 많이 쓰는 프로그램이 되었다. 각 나라마다 역법(쉽게 말하면 달력)이 다르지만, 추상 클래스를 이용해 그때마다 OS에 맞는 달력 객체를 생성하게끔 만들었기 때문에 딱히 손볼 코드는 없었다.

 

이제는 왜 추상화가 구체화보다 유연하다고 표현하는지 이해가 될 것이다. 비슷한 기능을 하는 클래스가 여러 개 있을 경우, 추상 클래스를 만들어 상속받게 한 후 사용하는 것이 여러 장점이 많으니 이를 충분히 활용해 효율적으로 코딩을 할 수 있도록 더 열심히 공부해보자!

'Java' 카테고리의 다른 글

인터페이스의 장점  (0) 2022.05.13
인터페이스 선언, 상속, 구현  (0) 2022.05.13
추상 클래스, 추상 메서드  (0) 2022.05.05
다형성의 장점  (0) 2022.05.04
instanceof 연산자  (0) 2022.05.03