추상화란 해당 객체에 대해 모든 것을 설명하는 것이 아닌 핵심(관심사)부분만 추려서 표현하는 것이다.
우리가 라이브러리를 사용할때 해당 라이브러리에 대해 모든것을 알고 사용하지 않는다. 단순히 내가 원하는 것에 대한 반환값만 받으면 된다. 이렇게 불필요한 정보는 노출시키지 않고 필요로 하는 정보만 노출시키는 것이 추상화이다.
또한 객체간의 계층관계가 있을때 이 객체를 하나로 묶어 다른 상위 객체를 만드는것도 추상화라고 할 수 있다.
강아지,고양이,사자라는 객체가 있고 각각의 색깔과 다리개수 울음소리를 알아야 한다고 하자.
class Dog {
int color;
int legCount;
void bark();
}
class Cat {
int color;
int legCount;
void meow();
}
class Lion {
int color;
int legCount;
void roar();
}
이런식으로 코드를 짤 수 있다. 이 코드에는 하나의 문제가 있다. 중복된 코드가 많다는 것이다. color,legCount 같은 공통 부분을 다른 객체안에 만들고 이를 받는것이 상속이다. Animal이라는 객체를 따로 분리해보자.
class Animal {
int color;
int legCount;
}
class Dog extends Animal{
void bark();
}
class Cat extends Animal{
void meow();
}
class Lion extends Animal{
void roar();
}
이렇게 다른 객체에서 정의된 속성을 받는것이 상속이다. 해당 코드의 장점은 코드의 중복을 줄여주고 코드 재사용률을 높혀준다.
다형성의 사전적 정의는 다음과 같다
같은 종의 생물이면서도 어떤 형태나 형질이 다양하게 나타나는 현상
이를 프로그래밍에 대입하면 같은 자료형에 여러가지 타입의 데이터를 대입하여 다양한 결과를 얻어낼 수 있는 성질이라고 할 수 있다. 말로는 어려울 수 있는데 코드로 예시를 들어보자
public class Phone{
void call();
void message();
}
public class SmartPhone extends phone{
void internet();
}
public class Main {
public static void main(String[] args) {
//smartPhone의 기능은 하지 못함
Phone phone1 = new SmartPhone();
Phone phone2 = new Phone();
}
}
SmartPhone이 Phone에게 상속을 받는 경우 위와 같이 Phone으로 정의된 phone1에 SmartPhone 인스턴스를 생성할 수 있다. 하지만 이 변수의 타입은 Phone이기에 SmartPhone의 기능은 하지 못한다. 하지만 같은 타입으로 묶을 수 있기에 Phone이라는 타입의 데이터를 받아야하는 경우 Phone에게 상속된 SmartPhone은 해당 매개변수에 들어갈 수 있게된다.
// 모든 핸드폰이 들어갈 수 있다.
ArrayList<Phone> phones = new ArrayList<>;
//스마트폰만 들어갈 수 있다.
ArrayList<SmartPhone> smartPhones = new ArrayList<>;
객체의 관점만 아니라 오버로딩/오버라이딩 같은 메서드 관점에서도 다형성은 존재한다.
캡슐화는 객체 내부 정보와 기능을 하나의 캡슐로 만들어 외부로 부터 보호하는것을 의미한다. 캡슐을 보면 실제로 안에 어떤 약이 있는지 자세히는 확인할 수 없다. 하지만 우리는 이 알약이 어떤 기능을 하는지는 알고 있다. 이렇듯 객체의 책임(왜 필요한지)에 중심을 두고 실제 내부 동작 원리나 데이터들은 감추는 것이 캡슐화이다.
public class Animal{
private boolean isMoved = True;
}
public class Lion extends Animal{
private int height;
private int weight;
private String color;
}
이런식으로 private를 이용하여 객체 내부 필드값을 외부에서 변경할 수 없게 할 수 있다. 기능상 외부에서 변경을 해야 한다면 구현상 getter,setter를 사용하여 간접적으로 접근하여 변경할 수 있다.
하지만 getter,setter를 모든 필드에 대하여 구현하면 캡슐화의 의미가 퇴색된다(외부에서도 마음대로 인스턴스의 값을 변경시킬 수 있으니까). 위의 Lion 객체에 있는 필드값은 외부에서 생성하는 것에 따라 값이 바뀔 수 있지만 Animal 객체에 있는 isMoved 값이 바뀌면 동물의 의미 자체를 잃어버리기 때문에 주의해야한다.
잘못된 경우를 보자
public class Main{
public static void main(String[] args){
Car car = new Car();
Driver diver = new Driver(car);
}
}
public class Car{
public void start(){
/*시동을 걸시 동작하는 내부 로직*/
System.out.println("시동을 겁니다");
}
public void move(){
/*앞으로 가면서 동작하는 내부 로직*/
System.out.println("앞으로 갑니다");
}
}
public class Driver{
private Car car;
public Driver(Car car){
this.car = car;
}
public void drive(){
car.start();
car.move();
}
}
Car객체 안에 메서드를 public으로 두어 Driver에서 dirve 메서드 호출시 실행되게 만들었다. 코드 실행에는 문제가 없다. 하지만 car객체의 메서드가 변경될시 dirve 메서드의 코드도 수정을 해야한다. 이처럼 객체간에 데이터를 쉽게 접근하게 두고 설계하면 유지보수 관점에서도 매우 힘들고 Car안에 각각의 메서드에 대해 자세히 알아야 한다. 우리의 관심사는 Car가 움직이는 것이지 어떤 순서로 어떻게 움직이는지는 중요하지 않다. 이 코드는 다음과 같이 수정 할 수 있다.
public class Main{
public static void main(String[] args){
Car car = new Car();
Driver diver = new Driver(car);
}
}
public class Car{
private void start(){
/*시동을 걸시 동작하는 내부 로직*/
System.out.println("시동을 겁니다");
}
private void move(){
/*앞으로 가면서 동작하는 내부 로직*/
System.out.println("앞으로 갑니다");
}
public void operate(){
car.start();
car.move();
}
}
public class Driver{
private Car car;
public Driver(Car car){
this.car = car;
}
public void drive(){
car.operate();
}
}
이런식으로 우리가 필요로 하는 관심사만 public으로 따로 빼고 설계하면 Driver는 car의 내부 로직에 대해 알 필요도 없고, Car 내부 메서드에 변화가 있을시 Car안에서 해결할 수 있어 유지보수도 편하다.