객체지향 프로그래밍 심화(상속/캡슐화/다형성/추상화)
JAVA 뿐만 아니라 모든 객체지향 프로그래밍에 적용 되는 핵심 원리가 있습니다. 상속, 캡슐화, 다형성, 추상화가 여기에 해당합니다.
# 상속(Inheritance)
상속이란? 상속은 기존의 클래스를 재사용해 새로운 클래스를 만드는 것이며, 두가지의 클래스 중 상위 클래스와 하위 클래스로 나누어 상위 클래스의 맴버들을 하위 클래스에 내려주는 것을 말합니다.
위와 같이 동물이라는 상위 클래스가 존재합니다. 모든 동물들은 새끼를 낳을 수 있고 걸을 수 있다는 기능을 가지고 있습니다. 물론 상태에 해당하는 필드 또한 상위 클래스에 위치할 수 있습니다.
그리고 각각 코끼리, 닭, 소가 이러한 상위 클래스를 상속 받은 하위 클래스에 해당합니다.
코끼리의 경우 상위 클래스의 기능을 모두 상속받고 더불어 "두꺼운 가죽으로 덮여 있다"라는 자신만의 특징을 가지고 있습니다. 상속받은 것 이외에 자신이 원하는 기능 또는 상태를 가질 수 있다는 것입니다. 닭 또한 다른 하위 클래스인 동물들과 다르게 날개가 있다는 자신만의 특징을 가지고 있고 소 또한 뿔이 있다는 특징을 가지고 있습니다.
물론 하위 클래스에서 이런 맴버들은 스스로 추가하지 않을 수 있습니다.
클래스 간 상속에는 extends 키워드를 사용합니다. 하위 클래스 이름 뒤에 extends와 상위 클래스를 적음으로써, 하위 클래스가 상위 클래스에게 상속 받는다는 것을 알려줍니다.
class Animal {
String name;
void Procreate(){
System.out.println("새끼를 낳는다.");
};
void walk(){
System.out.println("걷는다.");
}
}
class Elephant extends Animal {
String leather;
}
class Chicken extends Animal { // Animal 클래스로부터 상속
void fly(){
System.out.println("날 수 있다.");
};
}
class Cow extends Animal { // Animal 클래스로부터 상속
String horn;
}
public class Prac {
public static void main(String[] args){
Animal p = new Animal();
p.name = "동물";
p.Procreate();
p.walk();
System.out.println(p.name);
System.out.println();
Chicken ch = new Chicken();
ch.name="닭";
ch.Procreate();
ch.walk();
ch.fly();
System.out.println(ch.name);
}
}
새끼를 낳는다.
걷는다.
동물
새끼를 낳는다.
걷는다.
날 수 있다.
닭
상속의 장점은 다음과 같습니다.
- 작성된 클래스를 재활용하여 코드를 줄일 수 있습니다.
- 각 코드의 유지보수가 쉽고 신뢰성 있는 개발을 할 수 있습니다.
- 다형성 차원에서 확장 용이합니다.
# super 와 super()의 차이
super에 앞서 this와 this()의 차이를 먼저 알아야 합니다.
this는 자기 객체를 가리키는 참조 변수명으로 한 메서드 내에서 맴버 변수와 지역 변수의 이름이 같을 때 이를 구분하기 위한 용도로 활용 됩니다.
this()는 같은 클래스의 다른 생성자를 호출하는데 활용되며 생성자 내에서만 사용 가능하고 항상 첫줄에 위치해야 합니다.
super와 super() 또한 이와 굉장히 유사합니다. super 키워드는 상위 클래스의 맴버값에 접근하기 위해 사용합니다. 보통 포인터와 함께 사용하고 자신이 속해 있는 클래스의 인스턴스 변수와 상속하는 상위 클래스의 변수가 같을 때 구분을 위해 사용합니다.
super()은 this()와 유사하게 상위 클래스의 생성자를 호출하기 위하여 사용합니다. this()와 마찬가지로 생성자에서 사용해야 하며 생성자 첫째줄에 위치해야 한다는 공통점이 있습니다.
# 캡슐화(Encapsulation)
캡슐화란 특정 객체 안에 관련된 속성과 기능 등을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것을 말합니다. 즉, 자신이 원하는 정도로 외부의 접근을 제한한다는 뜻입니다. 이를 가능하게 하는 핵심이 바로 접근 제어자 입니다.
접근 제어자 | 클래스 내 | 패키지 내 | 다른 패키지 하위 클래스 |
패키지 외 |
Private | O | X | X | X |
Default | O | O | X | X |
Protected | O | O | O | X |
Public | O | O | O | O |
위 내용에 따르면 접근 범위는 Public > Protected > Default > Private 순으로 정리할 수 있습니다. 예를 들어 Private 메서드를 가진 하나의 클래스를 같은 패키지 속 또 다른 클래스에서 객체를 만들어 접근하고자 하여도 Private 접근 제어자를 가진 메서드는 사용할 수 없습니다.
* 다른 패키지의 하위 클래스란 상속을 의미합니다.
이처럼 우리는 여러 접근 제어자를 활용하여 특정 정보는 제어하고 특정 정보는 공유하는 등 정보의 노출 정도를 자유롭게 조절할 수 있게 되었습니다. 그러나 만약 이러한 캡슐화는 유지하면서 Private 등으로 제한된 정보에 접근해야만 한다면...가능한 방법이 없을까요?
이런 경우를 대비하여 자바는 Getter, Setter 메서드를 지원합니다. 다음은 Getter, Setter의 예시입니다.
public class Practice {
public static void main(String[] args) {
Person p = new Person();
p.setName("자바개발자");
p.setAge(26);
System.out.println("나의 이름은 " + p.getName());
System.out.println("나의 나이는 " + p.getAge());
}
}
class Person {
private String name; // 변수의 은닉화. 외부로부터 접근 불가
private int age;
public String getName() { // 멤버변수의 값
return name;
}
public void setName(String name) { // 멤버변수의 값 변경
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if(age < 1) return;
this.age = age;
}
}
나의 이름은 자바개발자
나의 나이는 26
위 코드에서 Practice 클래스가 Person 클래스의 객체를 생성하고 바로 Person.name을 통해 원하는 이름을 설정하고 싶어도 name 변수가 Private 접근 지정자로 설정되어 있기 때문에 바로 접근하면 에러가 발생합니다. 이때 필요한 것이 Setter, Getter 메서드들 입니다.
먼저 Setter메서드는 외부에서 오는 특정 데이터에 대한 정부를 set 즉, 설정해 준다는 의미를 가지고 있습니다. 위에서는 p.setName("자바개발자") 코드를 통해 Practice 클래스가 원하는 "자바개발자" 라는 이름을 Person 클래스의 name 변수 안에 설정해 줄 수 있었습니다. Private는 같은 클래스 내에서는 접근이 가능하기 때문에 setName() 메서드를 통합 접근은 가능합니다.
Getter 메서드의 경우 데이터를 가져온다는 get의 성향이 있습니다. 위 코드에서 setter 메서드들을 통해 설정한 이름과 나이 데이터를 다시 Practice 클래스로 가져오기 위해서는 getter 메서드들을 통해서만 반환 받는 것이 가능합니다.
# 다형성(Polymorphism)
다형성이란 하나의 객체가 여러가지 형태를 가질 수 있는 것을 의미합니다. 예를 들어, 하나의 상위 클래스가 있습니다. 그리고 이 상위 클래스에게 상속받는 One, Two, Three라는 이름의 클래스가 존재한다고 할 때,
상위클래스 참조변수 = new 하위클래스(One,Two,Three 중 하나);
위와 같이 상위 클래스 타입 참조변수에 하위 클래스의 객체를 참조할 수 있도록 허용한 것이 다형성이라고 할 수 있습니다.
한 가지 유의할 점은, 참조변수가 사용할 수 있는 멤버의 개수는 실제 객체의 멤버 개수보다 같거나 적어야한다는 점입니다.
# 다형성의 필요 이유
public class Practice {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buy(new Potato());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
customer.buy(new Carrot());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
}
}
class Vegetable {
int price;
public Vegetable(int price) {
this.price = price;
}
}
class Potato extends Vegetable {
public Potato() {
super(2000);
}
};
class Carrot extends Vegetable {
public Carrot() {
super(2500);
}
};
class Customer {
int money = 50000;
void buy(Vegetable vegetable) {
money = money - vegetable.price;
}
}
현재 잔액은 48000원 입니다.
현재 잔액은 45500원 입니다.
위 예시에서 다형성의 중요성을 확인할 수 있습니다. 먼저 Vegetable이라는 상위 클래스를 각각 Potato와 Carrot 하위 클래스가 상속받고 있습니다. 여기서 주의깊게 봐야하는 것은 Customer 클래스에 buy 메서드입니다. 메서드의 매개변수를 보면 상위클래스인 Vegetable 타입으로 되어 있습니다. 즉, 상위 클래스인 Vegetable은 물론이고 하위 클래스인 Potato와 Carrot의 객체들도 해당 위치에 올 수 있다는 것입니다.
실제로 main 메서드를 보면 각각 Potato와 Carrot 객체들이 들어가 있습니다. 들어온 객체들의 내부에는 상위 클래스의 생성자에 값을 넣는 super() 메서드가 존재합니다. super() 메서드를 통해 각각 다른 price 변수 값을 갖고 이를 바탕으로 Customer 클래스에 money에서 빼는 것 까지가 전체 코드의 실행 과정입니다.
이 예시에서 확실히 알 수 있듯이 다형성의 가장 큰 장점은 코드를 확연히 줄일 수 있다는 것입니다. 이는 객체 지향 프로그래밍의 궁극적인 목표와도 일맥상통하며 때문에 객체 지향 프로그래밍에서 가장 중요한 핵심원리는 다형성이라고 많은 사람에게 인식되어 있습니다.
# 추상화(Abstraction)
추상이란 구체적이다의 반댓말로 다시 말해 구체적인지 않다라고 말할 수 있습니다. 지금까지 저희가 만들었던 코드들은 구체적으로 어떤 메서드는 어떤 기능을 하는지를 다 적어주었다면 추상화가 적용된 코드는 무언가 미완성적이고 구체적이지 않을 것이란 걸 예측할 수 있습니다.
# abstract 제어자
추상화를 공부할 때 abstract 제어자는 절대 빼놓을 수 없는 개념입니다. abstract 제어자는 private,public 등과 다른 기타 제어자입니다. '추상적'이라는 의미를 갖고 있는 이 제어자는 일반적으로 클래스와 메서드에 함께 사용됩니다. 클래스와 함께 사용되면 추상 클래스(abstract class), 메서드와 함께 사용되면 추상 메서드(abstract method)라고 부릅니다.
abstract class Practice {
abstract void example();
}
위 코드가 추상 클래스와 메서드의 기본적인 형태입니다. 추상 클래스의 경우 반드시 추상 메서드를 하나 이상 가지고 있어야 하며, 추상 클래스로 객체를 생성하는 것은 불가능 합니다.
추상 메서드는 메서드 시그니처만 존재하고 바디가 없습니다. 즉, 클래스와 메서드 모두 구체적이지 않고 미완성된 형태라고 생각하면 될 것 같습니다.
public abstract class Vegetable {
public String kind;
public abstract void price();
}
public class Potato extends Vegetable {
public Potato() {
this.kind = "감자";
}
@Override
public void price() {
System.out.println("2500원");
}
}
public class Carrot extends Vegetable {
public Carrot() {
this.kind = "당근";
}
@Override
public void price() {
System.out.println("3000원");
}
}
public class Practice {
public static void main(String[] args) throws Exception {
Potato potato = new Potato();
potato.price();
Carrot carrot = new Carrot();
carrot.price();
}
}
추상화를 사용하는 가장 큰 이유는 새로운 클래스 작성에 매우 유용하기 때문입니다. 위 코드 Vegetable 추상화 클래스에 price 메서드 바디가 상속을 통해 새롭게 생성되는 것처럼 새롭게 정보를 작성하는데 있어서 추상화는 개발자에게 편리함을 제공합니다.
참고로 추상화 클래스를 상속 받았다면 반드시 추상화 메서드를 오버라이딩하여 구체적인 바디를 작성해야 합니다.
# final
이후에 나올 인터페이스 개념 전에 기초적으로 알아야 할 개념인 final 키워드 입니다.
final은 필드, 지역변수, 클래스, 메서드 등의 앞에 올 수 있습니다.
위치 | 의미 |
class | 상속 불가 |
method | 오버라이딩 불가 |
variable | 값 변경 불가 |
# 인터페이스(Interface)
전체적으로 인터페이스는 추상 클래스와 비슷한 형태를 가지고 있습니다. 객체 지향의 추상화를 구현한다는 점에서 비슷한 역할을 수행하지만 명확한 차이점을 가지고 있습니다.
먼저 추상 클래스가 일반 메서드와 맴버 변수를 포함할 수 있었던 반면, 인터페이스 안에는 오직 추상 메서드와 상수만 맴버로 가질 수 있습니다. 따라서 내부 모든 필드는 public static final을 자동적으로 포함하고 있고 메서드는 public abstract를 포함하고 있습니다.
public interface Practice {
public static final int count = 20;
int age = 26; // public static final 생략
public abstract String getCount();
String getAge(); // public abstract 생략
}
인터페이스도 클래스들 처럼 상속이 가능합니다. 단, 클래스들이 extends 키워드로 상속을 하는 반면 인터페이스는 implements 키워드로 상속을 진행합니다.
interface Animal {public abstract void cry();}
interface Pet {play();}
class Dog implements Animal, Pet { // 인터페이스 다중 상속
public void cry(){ //메서드 오버라이딩
System.out.println("멍멍!");
}
public void play(){ //메서드 오버라이딩
System.out.println("원반 던지기");
}
}
class Cat implements Animal, Pet { // 인터페이스 다중 상속
public void cry(){
System.out.println("야옹~!");
}
public void play(){
System.out.println("쥐 잡기");
}
}
public class MultiInheritance {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
dog.cry();
dog.play();
cat.cry();
cat.play();
}
}
인터페이스는 클래스와 다르게 다중상속이 가능합니다. 더 나아가 클래스 상속과 혼합해서 사용할 수 있습니다.
abstract class Animal {public abstract void cry();} // 추상 클래스
interface Pet {public abstract void play();}
class Dog extends Animal implements Pet { // Animal 클래스 & Pet 인터페이스 상속
public void cry(){
System.out.println("멍멍!");
}
public void play(){
System.out.println("원반 던지기");
}
}