[ JAVA ] 객체지향프로그래밍(OOP) - 추상클래스

- 자바를 온몸으로(1)

Posted by 자바니또 on December 10, 2020 · 9 mins read

개요

JAVA를 잘 안다고 생각하며 지냈던 지난 날을 반성하며, 여러 블로그와 책을 읽고 간단한 개인 프로젝트를 통해 JAVA를 온몸으로 느껴보았다.

느꼈던 것을 잊지 않기 위해, 다른 사람에게 이 느낌을 전달해 주고 싶은 마음에 직접 느낀 자바의 객체지향을 내 블로그에 녹여내려 한다. 내가 간단하게 만든 개인 프로젝트는 여기에 있다.


우리의 실세계

우리는 주변의 사람이나 사물을 인식할 때 이름이나 모델명 등 구체적인 것으로 인식하지 않는다. 사람, 동물 뿐만 아니라 내가 객체라고 생각했던 물, 이불, 옷 모든 것들은 추상적인 것이고, 우리는 그 자체로 인지가 가능하다.

이해를 돕기위해 덧붙이자면, 우리의 집에 또는 호텔에 있는 이불이나 의자에겐 각자 모델명이 있고 상표가있지만 우리는 그냥 이불, 의자라고 부르고 또 빨간 사과를 보고 그냥 사과라고 부른다. 그것을 홍옥이나 부사라고 먼저 부르는 사람은 없을 것이다.

왜 그럴까? 본능적으로 우리는 그렇게 인지하는 것 보다 추상적인 단어로 인지하는 것이 훨씬 쉽고 편하다는 것을 알고있다. 무언가를 인지할 때마다 그것이 무엇인지 분석해야한다면 얼마나 머리가 아프겠는가? 심지어 identity가 되는 명칭을 모른다면 부를 수 조차 없다.

자세한 것을 분석하지않고 추상적인 단어를 사용하면 많은 것이 편해진다. 우리는 처음보는 강아지를 볼 때마다 그것을 뭐라불러야할지 고민할 필요가 없다.그건 그냥 ‘강아지’이다. 우리집에 있는 똘망이도 강아지 옆집의 세바스찬도 강아지이다. 이것이 추상적인 단어의 강력한 힘이다.

이처럼 우리는 우리가 바라보는 실세계는 추상적인 것들이 모인 하나의 추상덩어리이기 때문에 추상화가 없이는 표현이 불가능하다.


JAVA의 실세계 모델링

실세계의 추상화의 힘은 JAVA에서도 강력하다. 복잡한 실세계를 JAVA는 추상 클래스가 모델링하기 쉽게 도와준다. 구체클래스와 추상클래스의 차이를 간단히 말하자면 강아지의 속성으로 {이름, 몸무게, 종}을 뽑아서 클래스로 만들었다면 그것은 추상클래스이다. 여기에 {“Choco”,15, “말티즈”} 와 같이 특정한 값이 들어간다면 그것이 구체클래스이다.

완전히 추상적인 클래스란 우리가 강아지들을 종에 관계없이 ‘강아지’라고 부르듯이, 그 클래스의 타입으로 유연하게 여러 객체를 가리킬 수 있어야 한다. JAVA에서는 interface키워드를 제공하여 추상클래스를 쉽게 만들 수 있도록 도와준다.

interface Puppy{
	public void drink(Water water);	//짖어라
}
class Choco implements Puppy{
	@Override
	public void drink(Water water){
		//구현	
	};	
}
class Berry implements Puppy{
	@Override
	public void drink(Water water){
		//구현
	};	
}

위에서 예로들었던 강아지를 코드로 짜보았다. ‘Puppy’는 추상클래스, ‘Choco’는 ‘Puppy’를 구현한 구현 클래스이자 구체 클래스이다. 추상클래스는 가져야 할 책임들을 미리 적어두는 계약서와 같다. Puppy는 물을 마시라는 메세지를 알아들어야 한다는 계약에 대한 책임이 생기고, Choco는 Puppy와 한 계약을 이행 할 구현(오버라이딩)에 대한 책임이 생긴다. 이것을 계약책임과 구현책임이라 한다. JAVA는 컴파일러가 컴파일 할 때 계약을 잘 이행했는지 검사해준다.

자! 우리는 Puppy라는 타입으로 Choco와 Berry를 모두 가리킬 수 있게 되었다. 이것은 추상클래스가 갖는 가장 중요한 특성이고 매우 강력하다. 이것의 강력함은 마지막에 말하기로 하고 우선 실질적으로 코딩할 때 어떤 점들이 편해지는지 알아보자.

우리가 Puppy를 만들지 않고 Choco와 Berry만 만들었다고 가정하자. 고객으로부터 다음과 같은 변경요구사항이 발생할 수가 있을 것이다.

1. Choco를 Coco로 변경해라.
2. drink()의 타입을 boolean으로 변경해라.
3. drink()를 drinkTo()로 변경해라.
4. drink()의 인자를 Milk타입으로 변경해라.
5. Cherry라는 클래스를 추가해라.

1. Choco를 Coco로 변경해야 한다면?

이 상황에서 우리가 해야할 일은 무엇일까? 현재 코드에서 new Choco()가 50번 사용되었다면 50번을 바꿔야 할 것이다. 이클립스와 같은 IDE를 사용한다면 물론 자동으로 바꿔주지만 그건 해결책이 아니다.

이것을 추상화로 어떻게 해결할 것인가? 고민을 하다보면 의문이 든다. Puppy puppy = new Choco()로 추상화를 이용해도 new가 사용되는 것은 마찬가지 아닌가? 아래와 같은 방법으로 해결이 가능하다.

abstract public class ChocoFactory{
	public static Puppy get(){ return new Choco(); }
}

ChocoFactory는 Choco객체의 생성에 대한 책임을 지는 메서드를 가지는 클래스이다. new Choco()대신 ChocoFactory.getChoco()를 50번 사용하자. Choco가 Coco로 변경되어도 우리는 ChocoFactory의 get()메서드 return문만 살짝 바꿔주면 된다. 물론 ChocoFactory라는 이름도 바꾸는 것이 좋겠지만 우선 컴파일에는 문제가 없다.

여기서 abstract에 대해 잠깐말하자면 interface와 같은 추상클래스이지만 일부의 메서드에 구현부가 추가된 것이다. Choco는 물을 마시라는 명령을 수행할 수 있어야 하지만어떻게 마실지는 Choco의 마음이다. abstract는 여기에 어떻게 마실지까지 정해주는 계약서이다. 때문에 abstract는 interface보다 유연성이 떨어진다고 할 수 있다.

다시 돌아와서 이러한 메서드를 FactoryMethod라 하는데 디자인 패턴의 FactoryMethodPattern과는 다르다. FactoryMethod에 대해서는 “자바API디자인” 이라는 책에 자세히 나와있으니 한 번 찾아보길 바란다. 여기서 좀 더 자세히 쓰고 싶지만, 글의 전체적인 주제가 흐려질 것 같아 이쯤하고 ,객체의 책임에 대한 포스팅을 할 때 적어보도록 하겠다.


2, 3, 4. drink()를 변경해야 한다면?

2번부터 4번까지는 모두 drink()를 변경하는 비슷한 상황이다. 추상클래스가 없는 상황에서 저러한 상황이 발생한다면 정말 끔찍하다. 코드가 길면 길수록 인내심은 짧아질 것이다. 우리는 여기저기 퍼져있는 코드들을 수집하여 하나하나 변경해주어야 한다. 코드를 만든 개발자는 비교적 쉬울 수 있지만, 코드를 받은 다른 개발자는 Choco를 찾아서 변경하고 Choco와 같은 구현부가 있는 클래스들도찾아서 변경해야한다. 남은 Puppy인지 Crab인지 코드를 까봐야만 알 수 있다. 개발은 혼자만 하는것이 아니라 협업이기 때문에 이 것을 항상 명심해야한다.

우리의 효자 추상클래스를 사용한다면 상황은 달라진다. Puppy만 수정해주면 수집은 컴파일러가 알아서 해준다. 우리는 컴파일러가 찾아준 오류만 찾아가서 수정해주면 된다. 실제로 나는 계약의 책임과 구현의 책임을 이용해서 처음으로 코딩했을 때, 지금까지의 내가 정말 바보였다는 깊은 반성을 하게되었다.


5. Cherry라는 클래스를 추가해야 한다면?

Cherry를 추가하려면 내가 만들 Cherry와 비슷한 애를 찾아서 비슷 하게 만들어야 한다. 개발자라면 다 알고있으니까 쉬운 일이겠지만 남은 그렇지 않다. 앞의 상황들과 마찬가지로 코드들을 살펴보고 분석을 어느정도 한다음에야 작업에 들어갈 수 있다.

추상클래스는 어떨까? Cherry를 추가하고 싶다면 우리는 implements 키워드로 Puppy를 구현하겠다고만 명시라면 컴파일러가 구현해야 하는 것들을 알려준다. 얼마나 편리한가?

우리는 Puppy를 만들지 않고 그냥 Choco라는 이름으로 클래스를 만드는 것이 만들 때는 훨씬 편하지만 수정할 때는 그렇지 않다. Choco라는 클래스가 변경가능성이 매우 높고 자주 사용된다면 추상클래스를 사용하는 것이 정신 건강에 좋을 것이아.

이상으로 5가지 상황에대해 추상클래스를 사용하면서 얻는 장점을 적어보았다. 추상클래스의 핵심은 추상클래스를 통해 구체클래스를 숨길 수 있다는 것이다. 사용자는 구체클래스의 내부가 어떻게 생겼는지 알 필요가 없다. 그저 추상클래스의 public method만 필요에 따라 호출하면 된다. 결국 추상클래스는 정보은닉을 통해 유연성과 이식성을 높여주는 도구라 볼 수 있다.

앞에서 말한 장점 말고도 내가 생각하기에 큰 장점 중 하나는 코드의 전체적인 흐름을 추상클래스들로만 테스트가 가능하다는 것이다. 개발의 속도와 직관성을 높여주고 수정또한 쉽게 도와주니 JAVA OOP의 뿌리라 할 수 있겠다. 보통은 꽃이라 하는데 왜 뿌리라 하는지 궁금한가? 다음에 JAVA OOP의 꽃인 다형성에 대해 포스팅 하겠다.

추상화의 강력함

interface Puppy{
	public void drink(Water water);	//짖어라
}
class Choco implements Puppy{
	@Override
	public void drink(Water water){
		//구현	
	};	
}
class Berry implements Puppy{
	@Override
	public void drink(Water water){
		//구현
	};	
}
class Client{
	public void doAction(List<Puppy> list, Water water){
		for(Puppy puppy : list){
			puppy.drink(water);
		}
	}
}

위의 코드는 Client에게 Puppy에 대한 List를 인자로 메시지를 전송하고 Client는 메시지를 받아 list안에 있는 puppy들에게 drink()메시지를 보낸다. 메시지에 대한 포스팅은 이 다음에 올릴 예정이니 객체에게 보내는 명령이라고만 알아두자.

우리는 하나의 Puppy타입으로 Choco와 Berry를 모두 가리킬 수 있게 되었고 그 결과, Puppy를 구현한 구현객체라면 그 모두에게 하나의 타입으로 묶어서 일괄적으로 같은 메시지를 보낼 수 있게 되었다. Client는 puppy가 Choco인지 Berry인지 신경쓸 필요가 없고 그것들은 모두 drink()라는 메시지를 수행할 수 있다는 것을 알기에 보내기만 하면 그 이후는 Choco와 Berry가 각자 알아서 수행할 것이다.

얼마나 강력한지 감이 오는가? 감이 오지 않는다면 없다고 가정해보자. Choco와 Berry뿐만 아니라 Cherry, Orange 등 50종의 강아지 클래스가 있다면 50개의 list와 50번의 for문을 써야한다. 코드의 중복을 확 줄여줌으로써 객체를 만드는데 부담을 줄여준다. 추상클래스가 OOP가 존재할 수 있게해주는 뿌리이다.


결론

  • OOP는 추상적인 것들을 변화의 리스크가 적게 프로그래밍하는 방법이다.

  • OOP는 빠르게 변하는 현대와 고객의 needs가 수시로 변하는 개발현장에서 추상클래스를 통해 높은 유연성과 높은 이식성을 제공함으로써 개발자를 편하게 해준다.

  • 추상클래스는 정보은닉 을 통해 유연성이식성을 높여주는 도구이다.

  • 추상클래스의 핵심은 여러 객체의 타입을 하나의 타입으로 가리킬 수 있다는 것이다.