본문 바로가기
프로그래밍/CS

[Computer Science] 객체 지향 프로그래밍 (OOP) 란 무엇일까?

by dev_gyu 2024. 12. 19.
728x90

# OOP 란?

객체 지향 프로그래밍(Object-Oriented Programming) 의 약자로, 프로그램을 객체 단위로 구성하는 프로그래밍 패러다임이다.

OOP 를 지원하는 언어로는 JAVA, Kotlin, Python, C++ 등이 있으며, OOP 를 사용하지 않는 언어로는 주로 저수준 언어, 절차적 언어가 존재한다.

 

OOP 는 주로 다음과 같이 4가지 주요 개념을 바탕으로 구성된다.

 

1. 캡슐화(Encapsulation)

상태(속성)와 동작(메소드)을 하나의 단위로 묶는 것

쉽게 생각해서 객체의 내부 구현을 외부에서 알아차리지 못하게 숨기고 접근을 제어하는 방식이다.

 

class Encapsulation(){
    private val value = 2
    
    fun run(): Int{
        return value
    }
}

val encapsulation = Encapsulation()

 

 

위에서 encapsulation 은 Encapsulation 클래스의 객체, 인스턴스이며 encapsulation 의 value 는 접근이 제한되어 있어 직접 호출하지 못한다.

 

2. 상속 (Inheritance)

기존 클래스 (부모 클래스, Super Class) 에서 새로운 클래스 (자식 클래스, Sub class) 를 만들 수 있는 기능

자식 클래스는 부모 클래스의 속성과 메소드를 상속받으며, 이를 확장하거나 재정의 할 수 있다.

open class SuperClass(){
    open val variable = 1

    open fun returnVariable(): Int = variable
}

 

위와 같은 Super Class 가 존재할때

class SubClass(): SuperClass(){
    override val variable: Int = 2

    override fun returnVariable(): Int {
        return super.returnVariable()
    }
}

 

이처럼 variable 변수를 재정의하고 returnVariable 메소드는 슈퍼 클래스의 코드를 재사용할 수 있고

class SubClass(): SuperClass(){
    override val variable: Int = 2
    private val newVariable: Int = 3

    override fun returnVariable(): Int {
    	val sumVariable = variable + newVariable
        return sumVariable
    }
}

 

이처럼 메소드를 재정의하여 새로운 값을 표현할 수 있다.

 

3. 다형성 (Polymorphism)

같은 메소드가 다양한 방식으로 동작하는 개념으로 주로 Overloading, Overriding 을 통해 구현된다.

즉, 상속을 통해 다형성을 구현할 수 있다.

 

이를 쉽게 이해할 수 있게 위의 코드를 다시 가져와보자.

open class SuperClass(){
    open val variable = 1

    open fun returnVariable(): Int = variable
}

 

다른 클래스들의 상속 대상이 될 Super Class 이다.

이를 다양한 클래스에 상속시켜보자

 

class SubClass1(): SuperClass(){
    override val variable: Int = super.variable

    override fun returnVariable(): Int {
        return 5
    }
}

class SubClass2(): SuperClass(){
    override val variable: Int = super.variable

    override fun returnVariable(): Int {
        return 10
    }
}

 

SubClass1 에는 returnVariable 의 return 값을 5, SubClass2 에는 10을 줬다.

이제 이 클래스들을 서로 다른 인스턴스를 생성하고 이를 프린트해보자.

val subClass1 = SubClass1()
val subClass2 = SubClass2()

println(subClass1.returnVariable()) // 5
println(subClass2.returnVariable()) // 10

 

모두 같은 SuperClass 를 상속하고 해당 메소드를 수행했음에도, 객체의 타입이 다르기에 프린트 된 내용이 달라졌다.

이렇게 각 객체가 다르게 동작할 수 있다는 점이 다형성의 핵심이다.

 

이렇게 적용될 수 있는 이유는 오버라이딩을 적용하는 경우 동적 바인딩 (Dynamic Binding) 이 적용되어 런타임에 동적으로 함수가 호출되기 때문인데, 오버라이딩을 하지 않고 오버로딩을 하는 경우 정적 바인딩 (Static Binding) 이 실행되어 컴파일 단계에서 메소드 호출이 결정된다.

 

즉, 내부 코드를 변환할 일이 없으면 코드를 override 하지 않고 정적 바인딩으로 적용하여 성능 이슈에 영향을 주지 않는 방향으로 구현하는게 좋다.

4. 추상화 (Abstraction)

불필요한 세부 사항을 숨기고 중요한 부분만 보여주는 것으로 객체의 상호작용을 명확히 하는 데 도움을 준다.

이러한 추상화의 대상으로 Abstract class 와 Interface 가 존재하는데 Abstract class 는 구현되지 않은 메소드, 구현 메소드를 모두 정의할 수 있고, Interface 는 메소드 시그니처만 정의하여 구현을 강제한다.

 

* 메소드 시그니처

- 메소드의 구성요소. 이름, 매개변수 타입, 반환 타입만을 포함 (코드 구현부는 포함하지 않음)

 


abstract class GyuAnimal {
    abstract fun speak()  // 추상 메서드, 클래스 상속시 구현이 강제된다.
    fun howling(){ // 일반 메소드, 이미 정의되어 있기 때문에 클래스 상속시에도 구현이 강제되지 않는다.
        println("아우우!")
    }
}

interface GyuAnimal2 {
    fun speak()
    fun howling() // interface 에서는 메소드 시그니처만 정의할 수 있음
}

class Dog : GyuAnimal() {
    override fun speak() {
        println("월월!")
    }
}

class Dog2 : GyuAnimal2 {
    override fun speak() {
        println("월월!")
    }

    override fun howling() {
        println("아우우!")
    }
}

fun main() {
    val dog = Dog()
    dog.speak()  // 프린트: 월월!
}

 

만약 모든 메소드의 구현을 강제하고 싶다면 Interface, 일부만 강제하고 싶다면 abstract class 로 구현을 해주면 된다.

 


객체 지향 프로그래밍은 위와 같은 4가지 핵심 개념을 토대로 시스템을 모듈화하여 개발할 수 있게 해주며 이로 인해 코드의 재사용성, 유지보수성, 확장성을 높이고 많은 언어에서 지원하고 있다.

728x90