프로그래밍 방식
1. 명령형 프로그래밍(Imperative Programming, IP)
- 프로그래밍의 상태와 상태를 변경하는 구문의 관점에서 연산을 설명하는 방식
- 무엇(What)을 ‘어떻게(How)’ 할 것인지에 집중한다.
- 1-1. 절차지향 프로그래밍(Procedural Programming, PP) : 문제를 순차적으로 처리하여 프로그램을 만드는 방식 (C, C++)
- 절차지향 프로그래밍은 함수를 구조화할 뿐 데이터 자체를 구조화하지 못하기 때문에, 소프트웨어의 규모가 커지거나 변화가 생기면 각각의 함수에 의존하는 부분을 매번 고쳐야 하는 등 작업이 복잡해져 스파게티 코드를 유지보수하는 데 어려움을 겪는다.
- 1-2. 객체지향 프로그래밍(Object-Oriented Programming, OOP) : 객체들 간의 상호작용을 통해 프로그램을 만드는 방식 (C++, Java, C#)
2. 선언형 프로그래밍Declarative Programming, DP)
- 프로그램이 무엇과 같은 지를 설명하는 방식
- ‘무엇(What)을’ 할 것인지에 집중한다.
- 2-1. 함수형 프로그래밍(Functional Programming, FP) : 수학적 함수의 계산을 통해 자료를 처리하고 함수를 조합하여 프로그램을 만드는 방식 (Kotlin)
객체 지향 프로그래밍
- 객체들이 서로 유기적으로 협력하고 결합하여 문제를 해결하는 컴퓨터 프로그래밍의 패러다임
- 프로그램을 객체들의 집합으로 구성하는 방법
- 데이터와 해당 데이터를 조작하는 함수를 객체 단위로 묶는다.
- 각각의 객체는 데이터와 함께 관련된 동작을 가지고 있고, 서로 상호작용하며 프로그램을 구성한다.
- 이때 객체의 속성(데이터)과 동작(메서드)을 정의하는 단위를 클래스(Class)라고 부른다.
- 장점 : 각 객체가 독립적인 역할을 가지기 때문에 코드의 변경을 최소화하고 유지보수를 하는 데 유리하다. 더 나아가, 코드의 재사용을 통해 반복적인 코드를 최소화하고, 코드를 최대한 간결하게 표현할 수 있다.
객체(Object)
- 사전적 정의 : 인식 가능한 물체/물건. 객체들은 각자의 고유한 속성과 동작을 갖고 있다.
- 소프트웨어 관점 : 서로 연관있는 변수(속성, property)들을 묶어놓은 데이터 덩어리
- 상태(필드)와 행동(메소드)로 구성된다.
- vs 인스턴스 : 클래스에 의해 생성된 실체로, 클래스의 정의를 바탕으로 실제 메모리 상에 할당된 객체를 의미한다.
객체 지향 프로그래밍의 4가지 특징
1. 다형성 (polymorphism)
- 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질
- 즉, 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할 수 있는 객체 지향의 특성
- 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것
- 즉, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것
- 동일한 인터페이스를 쓰면서도 객체의 타입에 따라 여러 다른 기능을 제공한다.
BEFORE
- 문제점 : Driver 클래스와 다른 두 개의 클래스가 서로 직접적인 의존 관계(의존성)를 가지고 있다.
- 이러한 상황을 '객체들 간의 결합도가 높다'고 표현한다.
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)
}
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)
}
- 결과 : 각 클래스의 내부 변경이나 다른 객체의 교체에 신경 쓰지 않고, 인터페이스에만 의존하여 수정이 필요할 때마다 코드 변경을 최소화할 수 있다.
- 그러나 여전히 실행 클래스의 코드에서 객체를 생성할 때, Vehicle 객체가 Car 과 MotorBike 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피하다.
- 이 문제를 해결하기 위해 등장한 것이 바로 의존관계 주입(dependency injection) 이다.
- 그러나 여전히 실행 클래스의 코드에서 객체를 생성할 때, Vehicle 객체가 Car 과 MotorBike 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피하다.
결합도
2. 캡슐화 (Encapsulation)
- 클래스 안에 서로 연관 있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
- 서로 관련 있는 데이터와 이를 처리할 수 있는 기능들을 한 곳에 모아 관리하는 것
- 데이터 보호(data protection) : 외부로부터 클래스에 정의된 속성과 기능들을 보호
- 데이터 은닉(data hiding) : 내부의 동작을 감추고 외부에는 필요한 부분만 노출
- 외부로부터 클래스에 정의된 속성과 기능들을 보호하고, 필요한 부분만 외부로 노출될 수 있도록 하여 각 객체 고유의 독립성과 책임 영역을 안전하게 지키고자 하는 목적
해당 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제한하는 방법
- 1. 접근제어자(가시성 변경자(visibility modifier))
- 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.
- 어떤 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다.
- 클래스 내부의 세부 구현을 숨겨서 외부 코드가 클래스의 내부에 의존하지 않도록 하는 것
- public(공개) : 멤버를 어디서나 볼 수 있다.
- internal(모듈 내부) : 멤버를 동일 컴파일 모듈 내에서만 볼 수 있다.
- protected(보호) : 멤버를 해당 클래스와 그 하위 클래스 안에서만 볼 수 있다.
- private(비공개) : 멤버를 해당 클래스 내부에서만 볼 수 있다.
변경자 | 클래스 멤버 | 최상위 선언 |
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()
// 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) // 3
println(parent.d) // 4
}
- 2. getter/setter 메서드
BEFORE
- 문제점 : Drvier 클래스가 Car 클래스의 세부적인 내부 로직을 알고 있다. 즉, 객체 간의 결합도가 높다.
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
- 해결책 : Car 클래스와 관련된 기능들은 온전히 Car 클래스에서만 관리되도록 하고, 불필요한 내부 동작의 노출을 최소화한다.
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()
}
3. 추상화 (Abstration)
- 사전적 의미 : 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것
- 불필요한 세부 사항들은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것
- 예를 들어, 서울의 지하철 노선도는 서울의 지리를 추상화시켜서 보여준다.
- 소프트웨어 관점 : 객체의 공통적인 속성과 기능을 추출하여 정의하는 것
- 추상 클래스와 인터페이스를 통해 어떤 객체가 수행해야 하는 핵심적인 역할만을 규정해 두고, 실제적인 구현은 해당 인터페이스를 구현하는 각각의 객체들에서 하도록 프로그램을 설계한다.
// 자동차와 오토바이의 공통적인 기능 추출하여 이동 수단 인터페이스에 정의
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 vs interface
4. 상속 (Inheritance)
- 하나의 자식 클래스가 상위에 있는 부모 클래스의 속성과 동작을 물려받는 개념
- 기존 클래스를 확장하거나 변경하지 않고도 새 클래스를 쉽게 정의할 수 있어 코드 재사용성이 높아지고, 클래스 간 계층 관계를 구조적으로 형성할 수 있다.
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() // 오토바이가 묘기를 부린다
}
오버로딩
- 같은 이름의 메소드를 매개변수의 타입이나 개수를 다르게 하여 여러 번 정의하는 것
- 같은 이름의 메소드가 다양한 입력에 대응할 수 있다.
오버라이딩
- 상속받은 메소드의 내용을 자식 클래스에서 변경하는 것
- 부모 클래스의 메소드를 자식 클래스에 맞게 재정의할 수 있다.
'안드로이드 > Kotlin' 카테고리의 다른 글
[Kotlin] Kotlin 이란 (0) | 2024.12.08 |
---|---|
[Kotlin] 추상 클래스(abstract class), 인터페이스(interface) (0) | 2024.12.08 |
[Kotlin] 함수형 프로그래밍(Functional Programming), 람다(lambda) (0) | 2024.12.06 |