사전적 정의 : 인식 가능한 물체/물건을 의미하며, 객체들은 각자의 고유한 속성과 동작을 갖고 있다.
소프트웨어 관점 : 서로 연관있는 변수(속성: property)들을 묶어놓은 데이터 덩어리
객체의 상태는 변수들을 통해 나타내며, 객체의 행동은 메서드를 통해 정의된다.
class Car {
// 속성 정의
var company: String = ""
var model = ""
var color = ""
var wheels = 0
var isConvertible = false
// 기능 정의
fun startEngine() {
println("시동을 건다")
}
fun moveForward() {
println("자동차가 앞으로 전진한다")
}
fun moveBackward() {
println("자동차가 뒤로 후진한다")
}
}
객체 지향 프로그래밍은 우리가 보고 인지하는 실제 세계를 흉내 내어 가장 기본적인 단위인 객체들을 만들고, 그것들 간의 유기적인 상호작용을 규정하여 프로그램을 발전시키는 프로그래밍 방법론이다.
또한 객체 지향적 설계를 통해 소프트웨어를 개발하면 코드의 재사용을 통해 반복적인 코드를 최소화하고, 보다 유연하고 변경이 용이한 프로그램을 만들 수 있다.
💡 Class : 객체의 설계도로 사용되며, 객체의 상태와 행동을 정의하는데 필드와 메서드를 포함할 수 있다. 상속을 통해 다른 클래스로부터 특성을 상속받을 수 있으며, 단일 상속만 허용한다.
💡 Interface : 일종의 추상된 틀로, 클래스에서 구현해야 하는 메서드들의 집합을 정의한다. 다른 클래스들이 해당 인터페이스를 구현함으로써, 공통된 행동이나 규약을 정의하고 일관성 있는 구조를 갖도록 도와준다. 클래스는 하나의 클래스만을 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있다.
WHEN) 다중 상속이 필요할 때, 클래스 간의 공통된 동작을 정의할 때, 구현체에서 특정 메서드가 반드시 존재하도록 강제할 때
💡 Abstract : 다중 상속을 지원하지 않고, 구현이 있는 메서드와 구현이 없는 메서드를 모두 가질 수 있으며, abstract 키워드가 붙은 추상 메서드만 하위 클래스에서 반드시 구현이 필요하다.
WHEN) 공통된 기능을 여러 클래스에서 공유할 때, 클래스 계층 구조를 형성할 때, 실제 인스턴스를 생성하지 않고, 클래스의 설계 목적으로만 사용할 때
사전적 정의 : 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것
핵심 : 불필요한 세부 사항들은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것
소프트웨어 관점 : 복잡성을 다루는 데 도움을 주는 방법으로, 공통 필드와 메서드를 정의하고 서브 클래스에서 이를 구체화함으로써 객체의 공통적인 특징을 표현하고 필요한 구체적인 동작을 제공한다.
// 자동차와 오토바이의 공통적인 기능 추출하여 이동 수단 인터페이스에 정의
interface Vehicle {
abstract fun start()
fun moveForward()
fun moveBackward()
}
// Car 클래스 : 이동수단을 구체화한 자동차 클래스
class Car : Vehicle {
override fun moveForward() {
println("자동차가 앞으로 전진한다")
}
override fun moveBackward() {
println("자동차가 뒤로 후진한다")
}
}
// MotorBike 클래스
class MotorBike : Vehicle {
override fun moveForward() {
println("오토바이가 앞으로 전진한다")
}
override fun moveBackward() {
println("오토바이가 뒤로 후진한다")
}
}
class Car {
var model = ""
var color = ""
var wheels = 0
// Car 클래스 고유의 속성
var isConvertible = false
fun moveForward() {
println("앞으로 전진한다")
}
fun moveBackward() {
println("뒤로 후진한다")
}
// Car 클래스 공유의 기능
fun openWindow() {
println("모든 창문을 연다")
}
}
class MotorBike {
var model = ""
var color = ""
var wheels = 0
// MotorBike 클래스 고유의 속성
var isRaceable = false
fun moveForward() {
println("앞으로 전진한다")
}
fun moveBackward() {
println("뒤로 후진한다")
}
// MotorBike 클래스 공유의 기능
fun stunt() {
println("오토바이로 묘기를 부린다")
}
}
// 추상화를 통한 상위클래스 정의
open class Vehicle() {
var model = ""
var color = ""
var wheels = 0
open fun moveForward() {
println("전진한다")
}
fun moveBackward() {
println("후진한다")
}
}
class Car() : Vehicle() {
var isConvertible = false
fun openWindow() {
println("모든 창문을 연다")
}
}
class MotorBike : Vehicle() {
var isRaceable = false
// 메서드 오버라이딩을 사용하여 상위 클래스의 기능을 재정의
override fun moveForward() {
println("오토바이가 앞으로 전진한다")
}
fun stunt() {
println("오토바이가 묘기를 부린다")
}
}
fun main() {
val car = Car()
val motorBike = MotorBike()
car.model = "테슬라"
car.color = "빨강색"
println("나의 자동차는 " + car.color + " " + car.model + "입니다")
car.moveForward()
motorBike.moveForward()
motorBike.moveBackward()
}
// 나의 자동차는 빨강색 테슬라입니다
// 전진한다
// 오토바이가 앞으로 전진한다
// 후진한다
사전적 정의 : 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질
소프트웨어 관점 : 한 타입의 참조변수(특정 클래스나 인터페이스의 타입으로 선언된 변수)를 통해 여러 타입의 객체(해당 클래스나 인터페이스의 하위 클래스의 객체)를 참조할 수 있도록 만든 것을 의미한다.
하나의 인터페이스나 부모 클래스를 통해 여러 구현 클래스나 자식 클래스를 다룰 수 있는 기능으로, 같은 메서드 호출을 통해 서로 다른 클래스의 메서드를 실행할 수 있다.
메서드 오버라이딩과 메서드 오버로딩을 통해 구현한다.
open class Animal {
open fun makeSound() {
println("Some generic sound")
}
}
class Dog : Animal() {
override fun makeSound() {
println("Bark!")
}
}
class Cat : Animal() {
override fun makeSound() {
println("Meow!")
}
}
fun main() {
// 한 타입의 참조변수를 통해 여러 타입의 객체를 참조
// 즉, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조
val animal1: Animal = Dog()
val animal2: Animal = Cat()
// 같은 메서드 호출을 통해 서로 다른 클래스의 메서드를 실행
animal1.makeSound() // Bark!
animal2.makeSound() // Meow!
}
// Vehicle 인터페이스
interface Vehicle {
abstract fun start()
fun moveForward()
fun moveBackward()
}
class Car : Vehicle {
override fun moveForward() {
println("자동차가 앞으로 전진한다")
}
override fun moveBackward() {
println("자동차가 뒤로 후진한다")
}
}
fun main() {
// val vehicles: Array<Vehicle> = arrayOf(Car(), MotorBike())
val vehicles: Array<Vehicle?> = arrayOfNulls(2)
vehicles[0] = Car()
vehicles[1] = MotorBike()
for (vehicle in vehicles) {
// println(vehicle::class)
println(vehicle?.javaClass)
}
}
class Driver {
fun drive(car: Car) {
car.moveForward()
car.moveBackward()
}
fun drive(motorBike: MotorBike) {
motorBike.moveForward()
motorBike.moveBackward()
}
}
fun main() {
val car = Car()
val motorBike = MotorBike()
val driver = Driver()
driver.drive(car)
driver.drive(motorBike)
}
// 전진한다
// 후진한다
// 오토바이가 앞으로 전진한다
// 후진한다
A클래스는 B클래스에 의존한다
라고 표현한다.객체들 간의 결합도가 높다
고 표현한다. 문제점 : 결합도가 높은 상태는 객체 지향적인 설계를 하는데 매우 불리하다. 추상화, 상속, 그리고 다형성의 특성을 활용하여 프로그래밍을 설계할 때 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램을 설계한다.
interface Vehicle {
fun moveForward()
fun moveBackward()
}
class Car : Vehicle {
override fun moveForward() {
println("자동차가 앞으로 전진한다")
}
override fun moveBackward() {
println("자동차가 뒤로 후진한다")
}
}
class MotorBike : Vehicle {
override fun moveForward() {
println("오토바이가 앞으로 전진하다")
}
override fun moveBackward() {
println("오토바이가 뒤로 후진한다")
}
}
class Driver {
fun drive(vehicle: Vehicle) {
vehicle.moveForward()
vehicle.moveBackward()
}
}
fun main() {
val car = Car()
val motorBike = MotorBike()
val driver = Driver()
driver.drive(car)
driver.drive(motorBike)
}
// 자동차가 앞으로 전진한다
// 자동차가 뒤로 후진한다
// 오토바이가 앞으로 전진하다
// 오토바이가 뒤로 후진한다
문제점 : 객체를 생성할 때 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피하다. 즉, 객체 간 결합도가 높다.
필드와 메서드를 하나의 단위로 묶어서 외부에서 직접적인 접근을 제한하고 데이터를 보호하기 위한 것으로, 필요한 경우에만 메서드를 통해 데이터를 조작할 수 있다.
정보 은닉화 : 객체의 외부와 내부를 구분하고, 외부로부터의 접근을 제어하여 객체를 안정적으로 사용하고 관리할 수 있도록 도와준다.
접근제어자와 getter/setter 메서드를 통해 구현한다.
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 하위 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
package package1
open class SuperClass {
private val a = 1
protected val b = 2
internal val c = 3
public val d = 4
open fun printEach() {
println(a)
println(b)
println(c)
println(d)
}
}
fun main() {
val superClass = SuperClass()
// println(superClass.a)
// println(superClass.b)
println(superClass.c) // 3
println(superClass.d) // 4
}
package package2
import package1.SuperClass
class SubClass : SuperClass() {
override fun printEach() {
// println(a)
println(b)
println(c)
println(d)
}
}
fun main() {
val parent = SuperClass()
// println(parent.a)
// println(parent.b)
println(parent.c)
println(parent.d)
}
class Car(private val name: String, private val color: String) {
fun startEngine() {
println("시동을 겁니다")
}
fun moveForward() {
println("자동차가 앞으로 전진한다")
}
fun openWindow() {
println("모든 창문을 연다")
}
}
class Driver(private val name: String, private val car: Car) {
fun drive() {
car.startEngine()
car.moveForward()
car.openWindow()
}
}
fun main() {
val car = Car("테슬라 모델X", "레드")
val driver = Driver("김코딩", car)
driver.drive()
}
// 시동을 겁니다
// 자동차가 앞으로 전진한다
// 모든 창문을 연다
문제점 : drive() 메서드가 호출되었을 때 Car 클래스의 메서드들이 순차적으로 실행되고 있다.
class Car(private val model: String, private val color: String) {
private fun startEngine() {
println("시동을 건다")
}
private fun moveForward() {
println("자동차가 앞으로 전진한다")
}
private fun openWindow() {
println("모든 창문을 연다")
}
fun operate() {
startEngine()
moveForward()
openWindow()
}
}
class Driver(private val name: String, private val car: Car) {
fun drive() {
car.operate()
}
}
fun main() {
val car = Car("테슬라 모델X", "레드")
val driver = Driver("김코딩", car)
driver.drive()
}
// 시동을 건다
// 자동차가 앞으로 전진한다
// 모든 창문을 연다