고양이는 동물이다(is-a), 자동차는 엔진을 가지고 있다(has-a)
is-a는 "~는 ~이다"라는 말이며 상속을 의미합니다. has-a는 "~는 ~을 가지고 있다"라는 말이며 구성을 의미합니다.
1. 상속이란?
상속은 클래스 간의 계층 구조를 형성하여 특성을 물려주는 구조입니다.
자바를 배우신 분들이라면 상속에 대해 아마 잘 아실 거라고 생각됩니다.
조금이라도 난이도가 있는 코드를 보면 extends라는 문법요소가 꼭 있습니다.
이때 extends가 바로 상속을 표현하기 위해 사용되는 문법 요소입니다.
코드
class Animal {
public void eat() {
System.out.println("동물이 먹는다.");
}
}
class Cat extends Animal {
public void meow() {
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
cat.meow();
}
}
상속의 특징
1. 코드 재사용
상속을 사용하게 되면 부모 클래스의 속성과 메서드를 자식 클래스에서 사용할 수 있으므로 코드의 중복이 줄어들고 유지보수에 용이합니다.
예시코드를 보면 Cat 클래스에서는 eat 메서드가 없는데 main 메서드에서는 멤버 접근 메서드를 이용해서 cat 객체의 eat메서드에 접근하게 됩니다. 이는 Cat 클래스에 부모클래스(Animal)의 코드를 사용한 것이므로 코드의 중복이 줄어든다.
2. 확장성
상속을 사용하게 되면 기존 클래스를 수정하지 않고도 기능을 추가하거나 변경할 수 있습니다.
class Cat extends Animal {
@Override
public void eat() {
System.out.println("고양이가 먹는다.");
}
public void meow() {
System.out.println("야옹~");
}
}
위 코드에서는 eat 메서드를 오버라이딩함으로써 부모클래스(Animal)를 수정하지 않고도 기능을 변경하였다.
meow 클래스는 부모 클래스에 없는 메서드인데 자식 클래스에서 추가함으로써 기능을 추가하였다.
3. 다형성
class Animal {
public void eat() {
System.out.println("동물이 먹는다.");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("고양이가 먹는다.");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("강아지가 먹는다.");
}
}
public class Main {
public static void main(String[] args) {
Animal myPet = new Cat();
myPet.eat(); // "고양이가 먹는다." 출력
myPet = new Dog();
myPet.eat(); // "강아지가 먹는다." 출력
}
}
다형성이란
객체지향 프로그래밍에서 한 형태가 여러 가지 기능을 가지는 것을 말합니다. 이는 같은 메서드 호출이지만, 실행되는 객체의 타입에 따라 다른 동작을 수행하게끔 할 수 있습니다.
위 코드에서 myPet.eat() 메서드는 두 번 호출되지만, 각기 다른 결과를 출력합니다. 이는 myPet 참조변수에 Cat 객체와 Dog객체가 각각 할당되었기 때문입니다. Cat과 Dog는 eat 메서드를 재정의 했기 때문에 eat메서드가 실행될 때 다른 값을 출력합니다. 이처럼, 다형성은 하나의 메서드나 클래스가 여러 가지 형태를 가질 수 있도록 해줌으로써 코드의 유연성과 확장성을 높이는데 도움을 줍니다.
4. 캡슐화 저해
class Animal {
protected int meal = 0;
public void eat() {
System.out.println("동물이 먹는다.");
}
}
class Cat extends Animal {
public void meow() {
meal += 1;
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
cat.meow();
}
}
Cat 클래스의 meow 메서드를 보면 Animal에서 정의된 변수인 meal에 접근하고 있음을 볼 수 있습니다.
이렇게 외부 클래스에서 내부 클래스에 접근을 하게 되면 캡슐화가 저해되고, 결합도가 올라가므로 유지보수하는데 어려움이 생깁니다.
그다음으로 구성에 대해 설명해 드리겠습니다.
2. 구성이란?
구성이란 한 객체가 다른 객체를 포함하는 관계입니다.
상속에 비해 구성이라는 단어는 생소한 것 같습니다.
하지만 막상 구성에 대해 알아보면 "아 이게 구성이었어?"이라는 생각을 하실 수 있습니다.
코드
class Engine {
public void start() {
System.out.println("엔진이 가동됩니다.");
}
}
class Car {
private Engine engine;
public Car() {
this.engine = new Engine();
}
public void startEngine() {
this.engine.start();
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.startEngine();
}
}
구성의 특징
1. 코드 재사용
구성을 사용하게 되면 다른 클래스의 객체를 통해서 코드를 재사용할 수 있습니다.
위 코드를 보면 Car 클래스에서 Engine 객체를 생성한 뒤, startEngine() 메서드에서 engine의 start 메서드를 재사용하는 모습을 볼 수 있습니다.
2. 유연성
유연성...?! 내가 아는 유연성은 허리가 잘 구부러지는 거였는데...
코딩에서 유연성은 소프트웨어 시스템이 변경에 대해 쉽게 대응할 수 있는 특징을 의미합니다.
class Engine {
public void start() {
System.out.println("엔진이 가동됩니다.");
}
}
class Car {
private Engine engine;
public Car() {
this.engine = new Engine();
}
public void startEngine() {
this.engine.start();
}
public void setEngine(Engine engine) {
this.engine = engine;
}
}
class ElectricEngine extends Engine {
@Override
public void start() {
System.out.println("전기 엔진이 가동됩니다.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.startEngine();
myCar.setEngine(new ElectricEngine()); // 중요하게 봐야하는 부분
myCar.startEngine();
}
}
myCar.setEngine(new ElectircEngine());을 보면 런타임시 동적으로 엔진을 변경하는 모습을 볼 수 있습니다.
이때 Engine을 상속하는 클래스들로 변경할 수 있으며 이는 다형성이라는 특징입니다.
구성은 다양성을 통해 코드의 유연성을 높여줍니다.
3. 상호 의존성
그러나 구성이 항상 좋은 것은 아닙니다. (코딩에 단점은 없고 장점만 있는 기능은 없다고 생각합니다)
구성을 하게 되면 구성 클래스가 다른 클래스의 객체를 포함하게 되므로, 한 클래스의 변경이 다른 클래스에 영향을 줄 수 있습니다. 예시코드를 보면 Car 클래스에서 Engine 객체를 포함하고 있습니다.
이때 Engine의 메서드나 변수가 변하게 된다면 Car클래스에도 영향이 미치므로 결합도가 높아집니다.
class Car {
private Engine engine;
3. 상속과 구성의 차이점
상속과 구성의 가장 큰 차이점은 어떻게 코드를 재사용하는가인 것 같습니다.
상속은 부모클래스의 코드를 자식 클래스에서 사용하는 것이고, 구성은 클래스 간의 협력을 통해 코드를 재사용할 수 있습니다. 또한 상속은 부모 클래스의 기능을 변경하거나 자식 클래스에서 추가하므로 확장을 하는데 구성은 구성 요소를 동적으로 변경하면서 확장을 제공합니다. (객체를 교환하면서 기능을 확장합니다)
마지막으로 상속은 is-a 관계이므로 하위 클래스가 상위 클래스에 포함된다는 의미이고, 구성은 has-a 한 클래스를 다른 클래스가 포함하고 있다는 의미입니다.