이 글은 예전에 쓴 OOP의 네가지 특징에 대한 글이 2300번이나 읽혀진 게 부끄러워서 작성하게 되었다.
그 글에는 오타도 있고 객체지향을 잘 정리해놓은 것도 아닌데 생각보다 많은 사람이 봐서 놀랐다.
이 글에서는 좀 더 객체지향을 잘 이해할 수 있도록 글을 작성할 예정이다. (물론 난 아직 객체를 100% 이해한 게 아니라 또 허접한 글일 수도 있다.)
내가 처음에 자바를 배웠을 때, 인터페이스랑 추상클래스의 차이가 궁금해서 찾아봤는데 인터페이스는 다중상속이 되고, 추상메서드만 가질 수 있고,,, 뭐 이런 것들이 나왔다.
그땐 인터페이스랑 추상클래스의 차이는 알겠는데...
그래서 뭐 어쩌라고?
라는 생각을 가졌던 것 같다. 그 어쩌라고를 설명해보겠다.
객체지향 프로그래밍(OOP)이란?
상태와 행동을 가지는 객체들이 서로 협력하는 구조로 프로그래밍 하는 것.
예를 들어, 자동차 클래스 Car
에 연료 fuel
랑 연료탱크용량 fuelTankCapasity
이라는 필드가 있다고 가정하자.
public class Car {
public int fuel; //연료
public int fuelTankCapasity; //연료탱크용량
}
여기서는 fuel
, fuelTankCapasity
을 public
으로 설정해서 외부에서 존재도 알 수 없게 만들었다.
근데! 여기서 문제가 있다. 외부에서는 fuel
와 fuelTankCapasity
값을 볼 수도 없고 수정할 방법도 없다.
public class Car {
private int fuel;
private int fuelTankCapasity;
public int getFuel() {
return fuel;
}
public void setFuel(int fuel) {
this.fuel = fuel;
}
public int getFuelTankCapasity() {
return fuelTankCapasity;
}
public void setFuelTankCapasity(int fuelTankCapasity) {
this.fuelTankCapasity = fuelTankCapasity;
}
}
하지만 이 방법은 객체지향적이라고 할 수 없다.
이 클래스로 객체를 만들어서 사용할 때, getter
와 setter
를 보고
아 이 클래스에는
fuel
랑fuelTankCapasity
라는 속성이 있군,,,
라고 생각을 하게 된다. 이렇게 열심히 private
으로 설정해서 감추려고 했는데 getter
와 setter
때문에 다 들통난 것이다...
이럴 때는 추상적인 메서드를 public
으로 제공해서 사용자가 속성은 모른채 속성들을 조작할 수 있게 해야 한다.
public class Car {
private int fuel;
private int fuelTankCapasity;
public int getPercentFualRemaining() {
return (fuel/fuelTankCapasity)*100;
}
}
남은 연료 퍼센트를 제공하는 메서드 getPercentFualRemaining()
를 만들었다.
이렇게 하면 직접적으로 속성을 안보여주면서 핵심적인 부분을 보게 할 수 있다.
( ➡️ 추상적으로 보여준다.)
클래스는 클래스를 상속 받을 수 있기 때문에 코드를 재사용 할 수 있다는 내용이다.
예를 들면, 고양이 Cat
클래스를 만들어야 한다고 생각해보자.
class Cat {
private String name;
public Cat(String name) {
this.name = name;
}
public void cry() {
System.out.println("냐옹냐옹!");
}
}
찾아보니 이미 만들어져 있는 Animal
추상클래스가 있다!!
Animal
추상클래스를 상속받으면 코드를 재사용할 수 있다.
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public abstract void cry(){
System.out.println("ㅜㅜㅜ");
}
}
class Cat extends Animal {
public void cry() {
System.out.println("냐옹냐옹!");
}
}
이건 Animal
추상클래스를 상속받는 Cat
클래스이다.
Animal
의 추상메서드인 cry()
메서드를 오버라이딩하였다.
이렇게 하면 Cat
클래스의 코드가 짧아진 걸 볼 수 있다.
그럼 Animal
추상클래스 대신 Animal
인터페이스를 만들어보자.
interface Animal {
public void cry();
}
이런식으로 만들 수 있을 것이다.
class Cat implements Animal {
private String name;
public void cry() {
System.out.println("냐옹냐옹!");
}
}
그럼 이렇게 인터페이스를 받은 클래스는 인터페이스의 추상메서드를 오버라이드 하면 된다.
근데 여기서 잠깐,,, 그럼 인터페이스를 안 받았았다면 Cat
클래스는 어떤 모습일까?
class Cat{
private String name;
public void cry() {
System.out.println("냐옹냐옹!");
}
}
아마 이런 모양일 것이다.
인터페이스를 받든 안받든 코드의 양이 줄어들지 않았다.
코드 재사용이 되었다고 말할 수 없다!
그럼 추상클래스는 코드 재사용을 하는 거고, 인터페이스는 무슨 역할을 하는 거지? 라는 생각이 들 것이다. 그건 추상화/다형성에서 설명하겠다.
추상화랑 다형성은 같이 이해하는 게 좋은 것 같다.
추상화
: 객체의 공통되는 부분을 추출해서 불필요한 부분은 감추자.
다형성
: 분명 똑같은 객체를 사용했는데 다르게 행동한다.
보통 이렇게 많이 정의가 되어있는데 이런 말은 30번 들어도 이해가 안될 수도 있다.
코드로 다형성이랑 추상화를 이해하는 게 좋다.
일단 추상화를 한 예시를 살펴보자.
이렇게 멤버와 가격을 받아 할인 가격을 return
하는 할인 정책 인터페이스를 만들고,
이 인터페이스를 implements
하는 클래스를 만들어보자.
public interface DiscountPolicy {
int discount(Member member, int price);
}
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000; //1000원 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
member
의 등급이 VIP이면 1000원을 할인해준다.
이렇게 할인정책이라는 것의 공통 속성을 빼내어 DiscountPolicy
라는 인터페이스를 만들어냈다. 이게 추상화!
근데 추상화를 해서 뭐가 좋은 걸까? 이건 다형성을 이해하면 알게 된다.
위 코드로 내가 작성했는데
갑자기 기획자가 와서
??? : 우리는 1000원 할인이 아니라 10% 할인으로 바꿀 겁니다~^^
라고 했다면 어떻게 해야 할까?
만약 추상화를 안해놨다면, DiscountPolicy
만 존재할 것이다.
public class DiscountPolicy {
private int discountFixAmount = 1000; //1000원 할인
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
그럼 결국 DiscountPolicy
의 코드를 수정해야 한다.
하지만 추상화를 잘 해놓았다면,,,?
public interface DiscountPolicy {
int discount(Member member, int price);
}
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000; //1000원 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; //10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
}
이런 식으로 RateDiscountPolicy
를 추가하면 될 것이다.
그러면 다형성을 이용해 객체를 사용해보자.
DiscountPolicy discountPolicy = new FixDiscountPolicy(); // 기존 1000원 할인정책
DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 바꾼 10% 할인정책
왜나하면 똑같은 타입의 객체지만 다르게 동작하기 때문이다. (이게 바로 다형성 !)
클래스와 다르게 인터페이스는 코드 중복을 없애는 게 아니라, 변경에 용이한 코드를 만들 수 있게 해준다.
그리고 이 DiscountPolicy
객체를 사용하는 입장에서는
인터페이스 타입 객체를 쓰고 있기 때문에 메서드가 어떻게 구현된 것인지는 알 수 없다. (이게 바로 추상화 !)
객체지향을 알게 되면 사실 상속
, 추상화
, 다형성
, 캡슐화
이 네 가지가 서로 연결되어서 구분지을 수 없다는 생각을 하게 된다.
결국 객체지향적인 설계
는 객체가 서로 영향을 받지 않아 내용을 변경했을 때 코드의 변경은 이루어지지 않고, 객체를 사용하는 사람(혹은 객체)에게는 내부 구현을 숨기면서 자료의 핵심을 보작할 수 있어야 한다는 것이다.
난 처음에 다형성을 이해해서 객체지향적인 설계가 무엇인지 조금이나마 알게 되었을 때, 정말 너무 신기하고 행복했다. 아 이래서 인터페이스가 존재하고,,, 클래스는 이런 거고 결국 좋은 설계는 객체들 간의 협력이구나라고 생각했는데 그게 생각을 하는 시간이 기뻤다.
누군가 이 글을 보고 단순히 추상클래스는 다중상속이 안되기 때문에 인터페이스가 만들어졌다~~~ 이런 걸 암기하는 게 아니라 객체지향을 "이해"하게 돼서 나와 같은 기쁨을 느낀다면 정말 좋을 거 같다.