https://www.inflearn.com/pages/infcon-2023-tech-oopfp
프로그램을 객체들의 집합으로 무엇을 어떻게 할 것인지에 집중하여 결과를 만들어내는 명령형 프로그래밍 방법
객체
객체지향 프로그래밍은 객체를 만들기 위해 설계도인 클래스를 작성하고, 부품에 해당하는 객체들을 만든 후, 이것들을 하나씩 조립 및 연결하여 전체 프로그램을 완성하는 것이다.
코드의 재사용을 통해 반복적인 코드를 최소화하고, 코드를 최대한 간결하게 표현할 수 있다.
코드의 변경을 최소화하고 유지보수를 하는 데 유리하다.
처리속도가 상대적으로 느리다.
객체가 많으면 용량이 커질 수 있다.
설계시 많은 시간과 노력이 필요하다.
사전적 의미 : 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것
불필요한 세부 사항들은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것
추상화는 객체의 공통적인 속성과 동작을 추출하여 정의하는 과정이다.
// 자동차와 오토바이의 공통적인 기능 추출하여 이동 수단 인터페이스에 정의
interface Vehicle {
abstract fun start() // abstract 키워드 생략 가능
fun moveForward()
fun moveBackward()
}
class Car : Vehicle {
override fun start() {
println("자동차가 시동을 건다")
}
override fun moveForward() {
println("자동차가 앞으로 전진한다")
}
override fun moveBackward() {
println("자동차가 뒤로 후진한다")
}
}
class MotorBike : Vehicle {
override fun start() {
println("오토바이가 시동을 건다")
}
override fun moveForward() {
println("오토바이가 앞으로 전진한다")
}
override fun moveBackward() {
println("오토바이가 뒤로 후진한다")
}
}
fun main() {
val car = Car()
car.start() // 자동차가 시동을 건다
car.moveForward() // 자동차가 앞으로 전진한다
car.moveBackward() // 자동차가 뒤로 후진한다
val motorBike = MotorBike()
motorBike.start() // 오토바이가 시동을 건다
motorBike.moveForward() // 오토바이가 앞으로 전진한다
motorBike.moveBackward() // 오토바이가 뒤로 후진한다
}
abstract class와 interface의 차이점
둘 다 객체 지향 프로그래밍의 추상화와 다형성을 구현하는 데 사용된다.
abstract class
하나 이상의 추상 메소드를 포함할 수 있는 클래스로, 구현 코드를 포함한 일반 메소드도 가질 수 있다.
단일 상속만을 지원한다.
공통된 기능을 재사용하면서도 일부 메소드의 구현을 자식 클래스에 위임하고자 할 때 주로 사용된다.
interface
상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있다.
반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있도록 한다.
상속은 하나의 자식 클래스가 상위에 있는 부모 클래스의 속성과 동작을 물려받는 개념이다.
BEFORE
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("오토바이로 묘기를 부린다")
}
}
AFTER
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) // 빨강색
println(car.model) // 테슬라
car.moveForward() // 전진한다
motorBike.moveForward() // 오토바이가 앞으로 전진한다
car.openWindow() // 모든 창문을 연다
motorBike.stunt() // 오토바이가 묘기를 부린다
}
오버로딩 : 같은 이름의 메소드를 매개변수의 타입이나 개수를 다르게 하여 여러 번 정의하는 것
오버라이딩 : 상속받은 메소드의 내용을 자식 클래스에서 변경하는 것
다형성은 한 타입(상위 클래스 타입)의 참조변수를 통해 여러 타입(하위 클래스)의 객체를 참조할 수 있도록 만드는 것이다.
BEFORE
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)
}
하나의 객체가 다른 객체의 속성과 기능에 접근하여 어떤 기능을 사용할 때, 의존하다
라고 표현한다.
즉, Driver 클래스와 다른 두 개의 클래스가 서로 직접적인 관계를 가지고 있는데, 이러한 상황은 객체들 간의 결합도가 높다.
결합도가 높으면 : 변경하고 검토해야 하는 모듈의 수가 많아져서 유지보수하기 어렵다.
해결책 : 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램을 설계해야한다.
AFTER
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: Vehicle = Car() // 높은 결합도
val motorBike: Vehicle = MotorBike() // 높은 결합도
val driver = Driver()
// 자동차가 앞으로 전진한다
// 자동차가 뒤로 후진한다
driver.drive(car)
// 오토바이가 앞으로 전진하다
// 오토바이가 뒤로 후진한다
driver.drive(motorBike)
}
클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
WHY
데이터 보호(data protection) : 외부로부터 클래스에 정의된 속성과 기능들을 보호
데이터 은닉(data hiding) : 내부의 동작을 감추고 외부에는 필요한 부분만 노출
해당 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제한하는 방법
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()
// private 멤버에 접근 불가능(에러)
println(superClass.a)
// protected 멤버에 접근 불가능(에러)
println(superClass.b)
println(superClass.c) // 3
println(superClass.d) // 4
}
package package2
import package1.SuperClass
class SubClass : SuperClass() {
override fun printEach() {
// private 멤버에 접근 불가능(에러)
println(a)
println(b)
println(c)
println(d)
}
}
fun main() {
val parent = SuperClass()
// private 멤버에 접근 불가능(에러)
println(parent.a)
// protected 멤버에 접근 불가능(에러)
println(parent.b)
println(parent.c)
println(parent.d)
}
BEFORE
class Car(private var name: String, private var 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()
}
AFTER
class Car(private var name: String, private var 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 getName() {
return name
}
fun drive() {
car.operate()
}
}
fun main() {
val car = Car("테슬라 모델X", "레드")
val driver = Driver("김코딩", car)
// 시동을 건다
// 자동차가 앞으로 전진한다
// 모든 창문을 연다
driver.drive()
}
SOLID 원칙은 총 5가지 원칙으로 구성되어 있다.
장점 : 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.
5가지 원칙이 필수인가요?
단일 책임 원칙은 클래스(객체)는 단 하나의 책임(기능)만 가져야 한다는 원칙이다.
즉, 하나의 클래스는 하나의 기능만을 담당하여 하나의 책임을 수행하는 데 집중되도록 클래스를 따로따로 여러개 설계하라는 원칙이다.
WHY : 만일 하나의 클래스에 기능(책임)이 여러개 있다면 기능 변경(수정)이 일어났을때 수정해야할 코드가 많아진다.
BEFORE
class EmployeeManagement {
// Create 작업을 담당하는 CRUD 메소드
fun addEmployee(employee: String) {
if (employee == "") {
postServer(employee) // 서버에 직원 정보를 보냄
logResult("[LOG] EMPLOYEE ADDED") // 로그 출력
} else {
logResult("[ERROR] NAME MUST NOT BE EMPTY")
}
}
// 서버에 데이터를 전달하는 메소드
fun postServer(employees: String) {}
// 로그를 출력하는 메소드
fun logResult(message: String) {
println(message) // 로그를 콘솔에 출력
writeToFile(message) // 로그 내용을 로그 파일에 저장
}
// 파일에 내용을 저장하는 메소드
fun writeToFile(msg: String) {}
}
AFTER
로깅만을 담당하는 클래스를 따로 분리하고, EmployeeManagement 클래스에서 합성하여 사용한다.
class EmployeeManagement {
private val logger = Logger() // 합성
// Create 작업을 담당하는 CRUD 메소드
fun addEmployee(employee: String) {
if (employee == "") {
postServer(employee) // 서버에 직원 정보를 보냄
logger.logResult("[LOG] EMPLOYEE ADDED") // 로그 출력
} else {
logger.logResult("[ERROR] NAME MUST NOT BE EMPTY")
}
}
// 서버에 데이터를 전달하는 메소드
fun postServer(employees: String) {}
}
class Logger {
// 로그를 출력하는 메소드
fun logResult(message: String) {
println(message) // 로그를 콘솔에 출력
writeToFile(message) // 로그 내용을 로그 파일에 저장
}
// 파일에 내용을 저장하는 메소드
fun writeToFile(msg: String) {}
}
개방 폐쇄 원칙은 클래스는 확장에 열려있어야 하며, 수정에는 닫혀있어야 한다는 원칙이다.
확장에 열려있다 : 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있다.
변경에 닫혀있다 : 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한한다.
추상화 사용을 통한 관계 구축을 권장한다는 의미이다.
BEFORE
class Animal(val type: String)
// 동물 타입을 받아 각 동물에 맞춰 울음소리를 내게 하는 클래스 모듈
class HelloAnimal {
fun hello(animal: Animal) {
when (animal.type) {
"Cat" -> println("냐옹")
"Dog" -> println("멍멍")
else -> println("알 수 없는 동물")
}
}
}
fun main() {
val hello = HelloAnimal()
val cat = Animal("Cat")
val dog = Animal("Dog")
hello.hello(cat) // 냐옹
hello.hello(dog) // 멍멍
}
AFTER
abstract class Animal {
abstract fun speak()
}
class Cat : Animal() {
override fun speak() {
println("냐옹")
}
}
class Dog : Animal() {
override fun speak() {
println("멍멍")
}
}
class Sheep : Animal() {
override fun speak() {
println("매에에")
}
}
class Lion : Animal() {
override fun speak() {
println("어흥")
}
}
// 기능 확장으로 인한 클래스가 추가되어도, 더이상 수정할 필요가 없어진다 (closed)
class HelloAnimal {
fun hello(animal: Animal) {
animal.speak()
}
}
fun main() {
val hello = HelloAnimal()
val cat: Animal = Cat()
val dog: Animal = Dog()
val sheep: Animal = Sheep()
val lion: Animal = Lion()
hello.hello(cat) // 냐옹
hello.hello(dog) // 멍멍
hello.hello(sheep) // 매에에
hello.hello(lion) // 어흥
}
리스코프 치환 원칙은 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다는 원칙이다.
다형성 원리를 이용하기 위한 원칙
다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메소드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미하는 것
BEFORE
abstract class Animal {
abstract fun move()
abstract fun speak()
}
class Cat : Animal() {
override fun move() {
println("고양이가 움직이다.")
}
override fun speak() {
println("냐옹")
}
}
class Dog : Animal() {
override fun move() {
println("개가 움직이다.")
}
override fun speak() {
println("멍멍")
}
}
class Fish : Animal() {
override fun move() {
println("물고기가 헤엄치다.")
}
override fun speak() {
throw Exception("물고기는 말할 수 없다.")
}
}
AFTER
abstract class Animal {
abstract fun move()
}
interface Speakable {
fun speak()
}
class Cat : Animal(), Speakable {
override fun move() {
println("고양이가 움직이다.")
}
override fun speak() {
println("냐옹")
}
}
class Dog : Animal(), Speakable {
override fun move() {
println("개가 움직이다.")
}
override fun speak() {
println("멍멍")
}
}
class Fish : Animal() {
override fun move() {
println("물고기가 헤엄치다.")
}
}
인터페이스 분리 원칙은 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야 한다는 원칙이다.
인터페이스의 단일 책임을 강조하는 것으로 인터페이스 분리를 통해 설계하는 원칙이다.
인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표이다.
BEFORE
interface ISmartPhone {
fun call(number: String)
fun message(number: String, text: String)
fun wirelessCharge()
fun AR()
fun biometrics()
}
class S21 : ISmartPhone {
override fun call(number: String) { /*...*/ }
override fun message(number: String, text: String) { /*...*/ }
override fun wirelessCharge() { /*...*/ }
override fun AR() { /*...*/ }
override fun biometrics() { /*...*/ }
}
class S3 : ISmartPhone {
override fun call(number: String) { /*...*/ }
override fun message(number: String, text: String) { /*...*/ }
override fun wirelessCharge() {
println("지원하지 않는 기능입니다.")
}
override fun AR() {
println("지원하지 않는 기능입니다.")
}
override fun biometrics() {
println("지원하지 않는 기능입니다.")
}
}
AFTER
interface IPhone {
fun call(number: String) // 통화 기능
fun message(number: String, text: String) // 문제 메세지 전송 기능
}
interface WirelessChargable {
fun wirelessCharge() // 무선 충전 기능
}
interface ARable {
fun AR() // 증강 현실(AR) 기능
}
interface Biometricsable {
fun biometrics() // 생체 인식 기능
}
class S21 : IPhone, WirelessChargable, ARable, Biometricsable {
override fun call(number: String) { /*...*/ }
override fun message(number: String, text: String) { /*...*/ }
override fun wirelessCharge() { /*...*/ }
override fun AR() { /*...*/ }
override fun biometrics() { /*...*/ }
}
class S3 : IPhone {
override fun call(number: String) { /*...*/ }
override fun message(number: String, text: String) { /*...*/ }
}
의존성 역전 원칙은 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙이다.
구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
BEFORE
class OneHandSword(val name: String, val damage: Int) {
fun attack(): Int {
return damage
}
}
class TwoHandSword { /*...*/ }
class BattleAxe { /*...*/ }
class WarHammer { /*...*/ }
class Character(
val name: String,
var health: Int,
var weapon: OneHandSword // 의존 저수준 객체
) {
fun attack(): Int {
return weapon.attack()
}
fun changeWeapon(newWeapon: OneHandSword) {
weapon = newWeapon
}
fun getInfo() {
println("이름: $name")
println("체력: $health")
println("무기: $weapon")
}
}
AFTER
interface Weaponable {
fun attack(): Int
}
class OneHandSword(val name: String, val damage: Int) : Weaponable {
override fun attack(): Int {
return damage
}
}
class TwoHandSword : Weaponable { /*...*/ }
class BattleAxe : Weaponable { /*...*/ }
class WarHammer : Weaponable { /*...*/ }
class Character(val name: String, var health: Int, var weapon: Weaponable) {
fun attack(): Int {
return weapon.attack()
}
fun changeWeapon(newWeapon: Weaponable) {
weapon = newWeapon
}
fun getInfo() {
println("이름: $name")
println("체력: $health")
println("무기: $weapon")
}
}