객체지향 프로그래밍은 4가지의 특징을 가지고 있습니다.
이러한 특징들은 자바 기본 서적에서 반드시 언급되며, 객체지향 언어로 개발을 경험한 분들은 이미 잘 알고 계실 것입니다.
저는 학부생 시절, 이러한 개념들을 이해하는 것이 정말 어려웠습니다. 공부를 하려고 검색을 해보면 아래와 같은 내용이 많이 나오곤 했습니다. 이해는 가지만 와닿지는 않았죠.
그래서 아래처럼 생각하고 넘어갔습니다.
구체적인 예시가 없으면 특정 정보를 받아들이기가 참 어렵습니다.
그래서 "추상화는 A다", "상속은 B다"와 같은 설명이 아닌 스타크래프트 세상에 존재하는 민씨가 팩토리에서 탱크를 생산하는 시나리오를 통해 객체지향 프로그래밍의 4가지 특징을 알아보려합니다.
스타크래프트의 세계에서는 팩토리에서 버튼만 누르면 자판기에서 음료수 뽑듯이 탱크도 뽑아줍니다.
순식간에 탱크를 뽑아주는 이 기계는 유닛을 생산하는 세 가지의 버튼이 있습니다.
이 엄청난 기계는 "짐 레이너"가 만든 "레이너 팩토리"와 "사라 케리건"이 만든 "케리건 팩토리"가 존재합니다.
레이너 팩토리
레이너 팩토리는 경량화 작업을 했기에 속도가 다른 탱크보다 빠릅니다. (T) 버튼을 누르면
Jim Raynor
라는 스티커가 붙은 탱크가 생산됩니다.
케리건 팩토리
케리건 팩토리는 경량화 작업을 하지 않은 순수한 탱크만을 생산합니다. (T) 버튼을 누르면
Ghost
라는 스티커가 붙은 탱크가 생산됩니다.
근처를 지나가던 민씨는 오늘따라 탱크가 가지고 싶어졌습니다.
팩토리에서 탱크를 생성하는 과정은 금속, 티타늄 등의 자원을 확보하고 이를 이용하여 부품을 만들고 조립한 뒤 완성된 탱크를 테스트까지 하는 매우 기술적이고 복잡한 작업입니다.
하지만 민씨는 복잡한 작업을 알 필요 없이 (T) 버튼으로 손쉽게 탱크를 생산할 수 있습니다.
즉 민씨는 팩토리가 탱크를 어떻게 생산하는 지는 알 필요가 없습니다.
이는 캡슐화에 해당합니다.
또한 팩토리는 민씨가 탱크를 생산하는 데 필요한 최소한의 정보 (T) 버튼만을 제공합니다.
즉 팩토리가 추상화를 통해 복잡한 프로세스 과정을 숨기고 있습니다.
이는 추상화에 해당합니다.
팩토리의 핵심 기능은 (V), (T), (G) 버튼에 해당하는 유닛의 생산에 있습니다.
따라서 레이너 팩토리와 케리건 팩토리는 이 핵심 기능을 반드시 구현해야합니다.
이는 상속에 해당합니다.
민씨는 (T) 버튼을 눌러서 탱크를 뽑았습니다.
레이너 팩토리에서는 Jim Raynor
스티커가 붙은 이동 속도가 2배인 경량화 작업이 완료된 탱크가 생산됐습니다.
케리건 팩토리에서는 Ghost
스티커가 붙은 순수한 탱크가 생산됐습니다.
민씨는 (T) 버튼을 누르는 동일한 행동을 했지만 세부 구현이 다른 두 종류의 탱크를 생산할 수 있습니다.
이는 다형성에 해당합니다.
위 시나리오를 바탕으로 한 코드를 통해 4가지 객체지향 프로그래밍 특징을 자세히 알아보겠습니다.
코드는 GitHub에서 확인하실 수 있습니다.
팩토리에는 벌처, 탱크, 골리앗의 세 유닛이 존재합니다.
Unit
클래스에서는 위 세 가지 유닛의 공통 속성을 추상화했습니다.
public class Unit {
private int offensePower; // 공격력
private int attackCycle; // 공격 주기
private double speed; // 이동 속도
private DamageType damageType; // 피해 유형
private int range; // 사거리
}
Factory
인터페이스는 팩토리에 필요한 핵심 기능을 추상화했습니다.
public interface Factory {
Vulture createVulture(); // 벌쳐 생산
Tank createTank(); // 탱크 생산
Goliath createGoliath(); // 골리앗 생산
}
Tank
인터페이스는 탱크가 가지는 기능을 추상화했습니다.
public interface Tank {
void siegeMode(); // 시즈 모드
void tankMode(); // 일반 모드
double getSpeed(); // 이동속도 반환
}
RaynorTank
, KerriganTank
는 Tank
인터페이스를 구현하고 Unit
클래스를 상속받았습니다.
public class RaynorTank extends Unit implements Tank {
// 구현 생략
}
public class KerriganTank extends Unit implements Tank {
// 구현 생략
}
RaynorFactory
, KerriganFactory
는 Factory
인터페이스를 구현하였습니다.
public class RaynorFactory implements Factory {
// 구현 생략
}
public class KerriganTank extends Unit implements Tank {
// 구현 생략
}
RaynorFactory
에서 RaynorTank
를 생성할 때 내부에서 어떤 작업을 수행하는 지는 외부에 노출되지 않습니다.
public class RaynorFactory implements Factory {
@Override
public Tank createTank() {
RaynorTank tank = new RaynorTank();
tank.performLightweightOperation(); // 경량화 작업
return tank;
}
}
따라서 User
클래스가 탱크를 생산하기 위해 할 수 있는 일은 createTank()
메서드를 호출하는 일 밖에 없습니다.
public class User {
public Tank buttonT() {
return this.factory.createTank();
}
}
유저 객체가 탱크를 얻는 방법은 buttonT()
메서드를 호출하는 것입니다.
class UserFactoryIntegrationTests {
@Test
void buttonT() {
// given
User user1 = new User(new RaynorFactory());
User user2 = new User(new KerriganFactory());
// when
Tank raynorTank = user1.buttonT();
Tank kerriganTank = user2.buttonT();
// then
assertThat(raynorTank.getSpeed()).isNotEqualTo(kerriganTank.getSpeed());
assertThat(raynorTank.getSpeed()).isEqualTo(kerriganTank.getSpeed() * 2);
}
}
kerriganTank
객체는 기본적인 이동 속도의 탱크를 생산합니다.
반면에 raynorTank
객체는 경량화 작업이 적용된 이동 속도가 2배 빠른 탱크를 생산합니다.
동일한 메서드 호출에도 다른 결과를 얻을 수 있습니다.
그래서 이렇게 만들면 뭐가 좋을까요?
위 예시로는 탱크만 생산하고 있습니다. 더 나아가서 벌처를 생산하고 싶을 땐 어떻게 하면 될까요?
public interface Vulture {
void useSpiderMine();
void activateIonThruster();
}
public class RaynorVulture extends Unit implements Vulture { }
탱크와 마찬가지로 벌처의 기능에 대한 인터페이스를 생성한 뒤 Unit
클래스를 상속받으면 됩니다.
유닛에 대한 공통적인 부분은 Unit
클래스에 있으므로 코드의 재사용성이 증가합니다.
위 시나리오에서 신생 회사 OtherFactory
가 등장하면 어떻게 될까요?
public class OtherFactory implements Factory {
@Override
public Vulture createVulture() { ... }
@Override
public Tank createTank() { ... }
}
레이너, 케리건과 같이 Factory
인터페이스를 구현하면 됩니다.
위의 세계관에서 전쟁이 일어나 조금 더 빠른 속력의 레이너 탱크가 필요해졌습니다. 그러면 어떻게 하면 될까요?
public class RaynorTank extends Unit implements Tank {
public void performLightweightOperation() {
// this.setSpeed(this.getSpeed() * 2);
this.setSpeed(this.getSpeed() * 3);
}
}
RaynorTank
의 경량화 메서드만 수정해주면 다른 코드를 건드리지 않아도 됩니다.
또한, 공격력이 높고 사거리가 긴 새로운 탱크가 필요하다면 어떨까요?
public class OtherTank extends Unit implements Tank {
@Override
public void siegeMode() {
// 공격력 사거리가 길게 구현
}
@Override
public void tankMode() {
this.init();
}
public void init() {
// 공격력 사거리가 길게 구현
}
}
Unit
클래스를 상속하고 Tank
인터페이스를 구현한 OtherTank
를 만들면 됩니다.
이렇게 된다면 User
코드의 변경 없이 buttonT()
를 이용하여 공격력이 높고, 사거리가 긴 탱크를 얻을 수 있게 됩니다.
User user = new User(new OtherFactory());
위 확장성 증가에서 알아본 것 처럼, 경량화 작업을 수정하려면 해당 클래스의 메서드만 확인하면 됩니다.
이러한 모듈화된 구조는 코드의 가독성을 향상시키고 유지보수를 더욱 쉽게 만듭니다.
변경이 필요한 부분을 찾기 쉽고, 새로운 기능을 추가하거나 수정할 때 개발자가 알기가 쉬워지는 셈이죠.
제가 생각하는 객체지향 프로그래밍의 핵심은 유연하고 확장가능한 재사용성 높은 소프트웨어를 만들어내는 것입니다.
기대효과를 보면 알 수 있듯이 객체지향의 4가지 특징을 잘 활용한다면 좋은 소프트웨어 설계가 가능해지는데요.
하지만 너무나도 추상적인 개념이라 학습이 어렵고 실제로 적용하는 것은 까다로운게 사실입니다.
그래도 지금까지 본 스타크래프트 세계의 가상의 시나리오와 이를 코드로 구현하는 과정을 보며 조금이나마 도움이 되셨으면 좋겠습니다.
감사합니다.