Java의 객체지향
객체지향 프로그래밍은 장점은 복잡성을 객체 안에 내재화시키고 객체간의 유연한 협력을 통해 프로그램을 단순하고 변경이 용이하게 만들어 준다는 것이다. 잘 알려진 객체지향 프로그래밍의 특징들은 이러한 장점을 잘 살릴 수 있도록 방향성을 제시해준다.
추상화
사전적인 의미의 추상화는 구체화와 반대되는 의미로 필요한 정보(목적)를 중심으로 간소화 시키는 것을 의미한다. 추상화를 설명하는데 흔히 드는 예시로 지하철 노선도가 있다. 지하철 노선도에서 보이는 역간 거리와 경로는 실제모습과 다르다. 노선도는 오직 역간의 관계를 중심으로 대락적인 지리를 표시하하였다. 이처럼 중요한 부분을 강조하고 불필요할 구체적인 부분은 제거하여 표현하는 것이 추상화이다.
프로그래밍 세계에서의 추상화는 공통적인 기능만 추출하여 역할을 정의하는 것이다. 자동차를 예시로 보자. 자동차는 앞으로 갈 수 있고 뒤로 갈 수도 있으며 좌/우회전을 할 수 있는 역할을 가진다. 이것이 공통적인 기능이다. 정의된 역할을 수행할 수만 있다면 승용차든 트럭이든 슈퍼카든 문제가 없다.
위 그림을 보면 자동차의 역할을 추상화 시켰다. 실제로 어떻게 동작하는지 구체화되어 있지는 않다.
❓ 그래서 왜 사용하는가?
이제 머리속에 위와 같은 궁금증이 생겼을 것이다. 추상화는 복잡성을 관리하고 단순화 시켜준다. 자동차를 이용하는 입장에서 생각해보자. 어떠한 사람이 먼 거리를 이동하고자 한다. 자동차로써의 역할을 할 수만 있다면 무엇으로 이동하든 상관이 없다. 어떻게 작동하는지 원리도 알 필요없다. 그저 앞으로 가고 뒤로가고 방향이동만 할 수 있으면 된다. 앞으로 가기 위해서 엔진이 동작하고 엔진이 바퀴를 굴리고 등 이러한 동작과정 자체를 알 필요가 없다는 것이다. 이러한 동작과정은 해당 역할을 배정받은 승용차, 트럭, 슈퍼카가 책임질 일이다.(이는 이후 설명할 캡슐화와도 연관이 깊다.) 정리하자면 결국 추상화는 사용자의 입장에서 알아야 할 것만 알면되도록 최소화해주는 것이다.
추상화의 힘은 복잡성의 단순화뿐만 아니라 대체가능성의 효과도 가지고 있다. 사용자가 역할에 의존한다면 해당 역할을 수행할 수 있는 모든 객체로의 변경이 가능하다. 쉽게 말해 사용자가 앞으로가고 뒤로가고 방향전환을 할 수 있는 모든 객체, 즉 슈퍼카든 트럭이든 승용차로든 언제든지 바꿀 수 있다는 것이다. 추상화 덕분에 객체지향 프로그램은 유연하고 변경에 용이한 설계를 할 수 있다.
이제 자바에서 추상화를 이용하는 방법에 대해서 알아보자. 자바에서 추상화는 interface
와 abstract class(추상 클래스)
를 이용할 수 있다.(두 친구는 각각의 목적과 사용방법이 다르다. 이에 관해서는 따로 포스팅할 예정이다) interface를 이용해서 위에서 설명한 자동차 예제를 코드로 작성해보자.
// src/Car.java
public interface Car {
void moveForward();
void moveBackward();
void turnLeft();
void turnRight();
}
// src/Truck.java
public class Truck implements Car {
@Override
public void moveForward() { /* 구현 */ }
@Override
public void moveBackward() { /* 구현 */ }
@Override
public void turnLeft() { /* 구현 */ }
@Override
public void turnRight() { /* 구현 */ }
}
// src/SuperCar.java
public class SuperCar implements Car {
@Override
public void moveForward() { /* 구현 */ }
@Override
public void moveBackward() { /* 구현 */ }
@Override
public void turnLeft() { /* 구현 */ }
@Override
public void turnRight() { /* 구현 */ }
}
// src/Main.java
public class Main {
private static void main(String[] args) {
Person person = new Person(new SuperCar());
Person person2 = new Person(new Truck());
}
}
Person 클래스를 보면 Car라는 interface
에 의존한다. 따라서 Car를 구현한 어떤 객체든(SuperCar와 Truck)을 사용할 수 있다. 이렇듯 추상화는 역할정의 부분과 그 구현체를 분리하여 보다 유연하고 변경에 용이한 프로그램을 만들 수 있도록 도와준다.
정리
- 추상화의 목표 - 구현의 복잡성을 배제하고 단순화시키기
- 추상화의 정의 - 공통적인 기능으로 역할을 만들어 정의한다.
- 추상화의 효과 - 역할 정의로 인한 대체가능성을 얻고 유연하고 변경에 용이한 프로그램을 구성할 수 있다.
- 추상화 방법 - 자바에서
interface
와abstract class
를 이용해 추상화시킨다.
캡슐화
앞서 추상화를 설명하면서 역할을 받은 객체가 구현할 책임을 진다고 하였다. 그리고 이러한 구현은 외부로부터 감춰진다. 트럭이 앞으로 가기위해 클러치를 떼고 기어를 바꿀 것이며 그로인해 엔진이 어떠어떠한 동작을 하는 것을 구현할 것이다. 만약 이 모든 부분이 외부에서 제어할 수 있다면 어떻게 될까? 동작하는데 사용되는 상태들과 부가적인 행위들을 외부에서 접근하고 호출하여 겨우 추상화로 복잡성을 단순화 해놓은 프로그램이 다시금 강한 연결을 맺게 되고 복잡해질 것이다. 이러한 문제를 해결하는 것, 데이터를 감춰 외부의 간섭을 최소화하고 객체가 능동적으로 동작하기 위한 자율성을 보장하는 것. 이것이 캡슐화의 목적이다.
여기서 추상화와 캡슐화의 연관관계가 보인다. 만약 역할을 정의하고 역할에 의존한다면 어떻게 될까? 사용자는 역할에 정의된 내용만을 알게 될 것이고 자동으로 그 이외의 부분은 감춰지게 된다. 이런식으로 객체지향의 특징들은 모두 유기적인 연관관계를 맺는다.
이러한 방식말고도 직접적으로 캡슐화를 할 수 있는 방법도 있다. 자바에서는 접근제어자
와 getter/setter
를 이용한다.
아래 코드를 보면 내부적으로 사용하는 상태들을 접근제어자 private
를 사용하여 감추고 getter 메서드만을 이용하여 값 확인만을 할 수 있도록 열어두었다.
public class Truck implements Car {
private int gearLevel;
private boolean isClutchOn;
private boolean isBreakOn;
public boolean getClutchStatus() {
return this.isClutchOn;
}
@Override
public void moveForward() {}
@Override
public void moveBackward() {}
@Override
public void turnLeft() {}
@Override
public void turnRight() {}
}
💡 객체가 능동적이고 자율성이 있다는 것
객체가 수행 해야할 일을 외부의 구체적인 명령없이 스스로 구현한다는 것을 의미한다. 외부의 간섭이 많아질수록 수동적인 객체가 되며 자율성을 잃어버리게 된다.
정리
- 캡슐화의 목표 - 복잡성을 객체 내부로 내재화(응집화)하고 외부의 간섭을 최소화 하기위함
- 캡슐화의 정의 - 외부로 필요한 부분만을 노출하고 내부 동작을 감추어 데이터를 보호하는 것
- 캡슐화의 효과 - 객체가 능동적으로 동작하며 객체간의 결합도(의존도)가 낮아진다. 즉, 궁극적으로 유연하고 변경에 용이한 설계가 가능해진다.
- 캡슐화 방법 - 외부에서는 추상화에 의존, 내부적으로는 접근제어자 및 getter/setter 사용
상속
상속은 기존 클래스를 재활용하여 변수와, 메서드를 물려받은 새로운 클래스를 구성하는 방법이다. 추상화에서 설명한 인터페이스와 비교해 공통기능을 역할 정의뿐 아니라 구현부까지 미리 작성할 수 있어 재활용성이 높고 인터페이스와 마찬가지로 대체가능성의 효과도 가지고 있다. 물론 상속받아 만든 클래스(이하 자식 클래스)는 재정의하여 구현부를 바꿀 수도 있다.
자바에서 extends
키워드를 이용해 상속을 받을 수 있고 부모 클래스의 모든 기능을 자식 클래스에서 사용, 혹은 재정의 할 수 있다.
public class Car {
void moveForward() { }
void moveBackward() { }
void turnLeft() { }
void turnRight() { }
}
public class Truck extends Car {
@Override
void moveForward() {
/* 재구현 */
}
}
상속
- 목적 - 공통된 기능을 재사용, 객체간의 계층적 분류
- 정의 - 기존 클래스를 재활용하여 변수, 메서드를 물려받은 새로운 클래스를 구성하는 방법
- 효과 - 활용도가 높은 대체가능성이 존재하는 구조구성
extends
키워드를 이용해 부모클래스를 상속받는다.
다형성
다형성은 객체지향의 꽃이라고 불릴만큼 매우 중요한 개념이다. 다형성이란 쉽게 다양한 형태를 가질 수 있다는 것이다. 실세계에서 우리는 각각 많은 역할을 가지고 있다. 부모님의 자식이면서 자식의 부모일 것이다. 또 다른 누군가의 친구일 수도 있다. 이처럼 객체지향에서도 객체나 객체의 속성 및 기능이 여러 역할을 가지고 상황에 따라서 다른 역할을 수행한다.
객체의 속성과 기능에 초점을 맞춰보자면 오버라이딩을 이용하여 기능의 역할을 재정의할 수 있으며, 오버로딩을 이용하여 같은 역할을 수행하는 여러 종류의 메서드를 만들 수 있다.
객체들의 관계를 중심으로 볼때, 다형성은 한 타입의 참조변수로 여러 가지 타입의 객체를 참조할 수 있도록 하는 것이다. 이러한 정의는 상속과 추상화 과정을 통해 이뤄낼 수 있다. 참조변수가 역할을 정의한 interface
에 의존하거나 부모타입 클래스를 가리킨다면 그 서브타입을 가지는 객체들로 언제든 변경시킬 수 있다. 정리하자면, 보다 일반적인 개념을 채택함으로써 그 일반성에 부합하는 객체들(같은 집합 내에 속하는 서브타입 객체들)을 참조할 수 있게 한는 것이다.
다형성을 통해서 우리는 객체들간 직접적인 결합을 피하여 느슨한 관계를 만들어 보다 유연하고 변경에 용이한 프로그램 설계가 가능해진다.
정리
- 목적 - 유연하고 변경에 용이한 프로그램을 설계하기 위함
- 정의 - 객체나 객체의 속성 및 기능이 여러 역할을 가지고 상황에 따라서 다른 역할을 수행하도록 설계하는 것
마무리
객체 지향의 4가지 특징은 유연하고 변경에 용이한 설계라는 동일한 목표를 위해 존재하며 각각이 따로 떼어놓고 생각하기 어려울만큼 밀접한 관계를 가지고 있다. 객체지향으로 설계한다는 것은 매우 어려운 내용이기에 그 내용을 최대한 자세하게 설명하기 위해 4가지 특징으로 분리하여 설명했다고 생각한다. 지금 바로 그 본질을 체화시킬 수는 없겠지만 항상 생각하며 좋은 설계를 할 수 있도록 노력해보자.
댓글남기기