객체지향 프로그래밍이 도대체 뭘까?
객체지향 프로그래밍 (Object-Oriented Programming)은 프로그램 설계방법론의 일종으로, 명령형 프로그래밍에 속한다. 프로그램을 객체 단위로 나누어 상호작용을 서술하는 방식이다.
다음과 같은 예시가 있다고 가정하자
fun main() {
Cloth("헤비웨이트 반팔 티셔츠", "이너 웨어", "L").description()
OuterWear("오버핏 가디건", "아우터 웨어", "XL", 34500, "가디건").also {
it.description()
it.price()
}
output(Cloth("헤비웨이트 반팔 티셔츠", "이너 웨어", "L"))
output(OuterWear("오버핏 가디건", "아우터 웨어", "XL", 34500, "가디건"))
}
fun output(cloth: Cloth) {
cloth.description()
}
interface Price {
var price: Int
fun price()
}
open class Cloth(
protected open var name: String,
protected open var type: String,
protected open var size: String
) {
open fun description() {
println("이 옷의 이름은 ${name}이며, $type ${size}사이즈 입니다.")
}
}
class OuterWear(
override var name: String,
override var type: String,
override var size: String,
override var price: Int,
private var detail: String
) : Cloth(name, type, size), Price {
override fun description() {
println("이 옷의 이름은 ${name}이며, $type - $detail ${size}사이즈 입니다.")
}
override fun price() {
println("이 옷의 가격은 $price 입니다.")
}
}
메인이 있는 클래스(메인함수와 output 함수)와 Cloth 클래스, OuterWear 클래스, Price 인터페이스 총 4가지가 존재한다.
옷 클래스, 인터페이스가 부모이며 아우터웨어가 자식 클래스로써 상속받고 있다.
interface Price {
var price: Int
fun price()
}
가격이라는 인터페이스는 가격 변수와 구현부가 존재하지 않는 price() 함수를 지니고 있다.
이는 추상화를 위한 것으로, 추상화는 형식만 선언하고 실제 구현은 자식 클래스에 일임하기 때문이다.
open class Cloth(
protected open var name: String,
protected open var type: String,
protected open var size: String
) {
open fun description() {
println("이 옷의 이름은 ${name}이며, $type ${size}사이즈 입니다.")
}
}
옷이라는 클래스는 이름, 타입, 사이즈라는 멤버 변수를 가지고 있으며 설명이라는 함수를 가지고 있다.
옷 클래스에 접근할 경우 주 생성자에 의해 이름, 타입, 사이즈 변수 값을 써넣어 줘야한다.
+++
kotlin에서는 상속을 위해 클래스 앞에 open을 붙인다.
java에서는 평범히 상속이 되나 static final을 붙이면 상속이 되지 않는다.
즉, kotlin에서는 final이 default인 상태인데, 이는 kotlin 공식 문서에도 나와 있는 부분이며 상속을 위해선 open 키워드를 사용해야한다고 한다.
final이 default가 된 이유에 대해서는 final 클래스, 멤버 함수는 컴파일 시점에 정적 바인딩을 통해 다형성을 처리하여 런타임에 체크를 하지 않아 성능 향상과 코드의 예측 가능성을 제공하게 된다. 나아가 open 키워드를 명시적으로 사용하여 클래스와 멤버 함수를 확장 가능하게 만들 수 있고, 이 경우에만 동적 바인딩이 적용되어 런타임에 다형성 체크가 이뤄진다.
더 효율적인 컴파일러 최적화를 통해 불필요한 가상 호출 메커니즘을 건너 뛰고 직접 호출함으로써 성능향상을 기대한다는 것과 예기치 않은 동작을 방지하여 안정성을 향상시킨다는 점 때문에 default가 되었다고 볼 수 있겠다.
+++
class OuterWear(
override var name: String,
override var type: String,
override var size: String,
override var price: Int,
private var detail: String
) : Cloth(name, type, size), Price {
override fun description() {
println("이 옷의 이름은 ${name}이며, $type - $detail ${size}사이즈 입니다.")
}
override fun price() {
println("이 옷의 가격은 $price 입니다.")
}
}
옷 클래스와 가격 인터페이스를 상속받은 아우터웨어 클래스이다.
옷 클래스의 멤버변수 3개와 함수 1개, 가격 인터페이스의 가격 변수와 함수 1개를 상속 받았다.
옷 클래스와 아우터웨어 클래스의 변수와 함수는 내부에서만 접근이 가능하며 외부 클래스에서 접근하고자 하는 경우, 다음과 같이 객체를 생성하여야 한다.
가격 인터페이스의 경우 컴패니언 오브젝트를 추가하면 접근 가능할지 몰라도, 이 외의 경우엔 상속받아야 접근이 가능하다.
fun main() {
Cloth("헤비웨이트 반팔 티셔츠", "이너 웨어", "L").description()
OuterWear("오버핏 가디건", "아우터 웨어", "XL", 34500, "가디건").also {
it.description()
it.price()
}
output(Cloth("헤비웨이트 반팔 티셔츠", "이너 웨어", "L"))
output(OuterWear("오버핏 가디건", "아우터 웨어", "XL", 34500, "가디건"))
}
메인 함수에서 옷이라는 클래스를 인스턴스 생성을 통해 초기 생성자를 통해 멤버 변수를 초기화 및 선언시킨 뒤, 설명 함수를 불러들이는 모습이다.
output() 함수를 사용해 불러오는 부분은 다형성을 테스트하는 것이다.
결과적으론 output에서 각 클래스의 description() 함수를 불러오는 것이 똑같다.
위 예시에서 사용된 3요소는 캡슐화 - 정보은닉, 상속, 다형성이 존재한다.
객체지향 프로그래밍의 3요소
- 캡슐화(encapsulation)
변수와 함수를 하나의 단위로 묵는 것. 보통 클래스를 통해 구현되며 해당 클래스의 인스턴스 생성을 통해 클래스 안에 포함된 멤버 변수와 함수에 쉽게 접근할 수 있다.
- 정보 은닉(information hiding) - 프로그램의 내부 구현을 감추고 모듈 내 응집도를 높이며, 외부의 노출을 줄여 모듈간 결합도를 떨어트림으로 유연함과 유지보수성을 높이는 것이다. 보통 public, protected, private를 보면 이게 뭔지 쉽게 알 수 있다.
- 상속(inheritance)
자식 클래스가 부모 클래스의 특성과 기능을 그대로 물려받는 것으로, 기능 일부분을 변경해야할 경우 자식 클래스에서 상복받은 그 기능만을 수정해서 다시 정의할 수 있다.
- 추상화(Abstraction)
객체 지향 관점에서 클래스를 정의하는 것으로 불필요한 정보 외 중요한 정보만 표현함으로써 공통의 속성과 기능을 묶어 이름을 붙이는 것이다.
- 다형성(polymorphism)
하나의 변수, 또는 함수가 상황에 따라 다른 의미로 해석될 수 있는 것을 말한다.
다른 클래스들이 같은 이름의 속성과 메서드를 가지고 있을 때, 같은 방식으로 이들에 접근하거나 이들을 조작하는 코드를 작성할 수 있게 해준다.
- 서브타입 다형성
- 매개변수 다형성 - 템플릿, 제네릭
- 임시 다형성 - 함수 오버로딩, 연산자 오버로딩
- 강제 다형성 - 묵시적 형 변환, 명시적 형 변환
장점: 유지보수에 용이하다, 높은 가독성을 지닌다, 코드 재사용에 용이하다.
단점: 설계가 어려워진다, 성능하락이 존재한다.
객체지향 프로그래밍의 5가지 설계 원칙 (SOLID)
- SRP(Single Responsibility Principle): 단일 책임 원칙
클래스는 단 하나의 책임을 가지고 그에 대한 책임을 져야 한다.
- OCP(Open Close Principle): 개방-폐쇄 원칙
확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
즉, 클래스를 수정해야 한다면 해당 클래스를 상속하여 수정해야 한다.
- LSP(Liskov Substitution Principle): 인터페이스 분리 원칙
각 행위에 대한 인터페이스는 서로 분리되어야 한다.
ex) 핸드폰으로 전화를 하는데 핸드폰 카메라가 방해 되면 안된다는 뜻
- DIP(Dependency Inversion Principle): 의존 역전 원칙
상위 클래스가 하위 클래스에 의존하면 안된다.
즉, 기본적인 공통 속성을 하위 클래스에 의존시키면 안된다는 것.
-잡설-
여기까지가 기본적으로 책이나 강의를 통해 배우는 교과서적인 내용이다.
하지만 홀로 공부를 하고 사측에서 요구하는 객체지향적 프로그래밍이란 무엇인가에 대한 질문, 과연 나는 객체지향적으로 코드를 짜고 있는가라고 묻는다면 난 정말 모르겠다. 일전에 지인분과 대화할 때도 경력이 있는 자신도 객체지향이 뭐냐고 갑자기 물어보면 솔직히 당황스럽다고 느낀다 했다.
위처럼 객체지향에 대한 사전적 의미, 요소, 원칙, 예제, 장단점을 공부하다보면 알게 될지, 실제로 코드의 작성을 거듭해나가며 자연스레 숙달해나갈지는 아직 모르겠다.