출처 Freepik
객체 지향 프로그래밍 (Object-Oriented Programming, OOP)
객체 지향 프로그래밍은 말 그대로 소프트웨어를 객체(Object)라는 독립적인 단위로 나누고, 이러한 객체들의 상호작용으로 소프트웨어를 구성하는 방법론입니다.
객체 지향 프로그래밍의 4대 개념
객체 지향 프로그래밍에는 중요한 4가지의 개념이 존재합니다. 바로 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism), 추상화 (Abstraciton)입니다.
캡슐화(encapsulation)
캡슐화는 변수(Variables)와 메서드(Methods)를 하나의 객체(Class)안에 묶는 것을 의미합니다.
이는 상태와 행동을 하나의 객체 안에 모아서 객체의 내부를 외부에 감추는 것을 의미합니다. 외부에서 객체의 내부에 접근하려면 해당 객체의 인스턴스를 생성하고 메서드를 통해 접근해야 합니다.
Kotlin에서는 변수의 접근을 `private` 접근 제어자를 통해 제한하고 메서드로 이를 접근하고 수정합니다.
class Person {
private var name: String = ""
private var age: Int = 0
fun getName(): String {
return name
}
fun setName(newName: String) {
name = newName
}
fun getAge(): Int {
return age
}
fun setAge(newAge: Int) {
age = newAge
}
}
fun main() {
val person = Person()
person.setName("John")
person.setAge(25)
println("Name: ${person.getName()}") // Name: John
println("Age: ${person.getAge()}") // Age: 25
}
이렇게 함으로써 정보 은닉을 할 수 있게 됩니다.
정보 은닉이란??
정보 은닉은 객체에 대한 정보를 노출시키지 않는 기법입니다. 캡슐화는 정보 은닉을 할 수 있는 방법 중 하나입니다. 그렇기 때문에 '캡슐화 = 정보 은닉'은 아닙니다.
상속(inheritance)
상속은 객체가 다른 객체의 특성과 기능을 그대로 물려받는 것을 말합니다. 클래스의 관점에서 말하자면 자식 클래스(subclass)가 부모 클래스(superclass)의 자원을 상속을 통해 물려받을 수 있게 됩니다.
// 부모 클래스
open class Bird(val name: String, val color: String) {
open fun fly() {
println("$name, the $color bird, is flying")
}
open fun tweet() {
println("$name, the $color bird, is tweeting")
}
}
// 자식 클래스
class Parrot(name: String, color: String) : Bird(name, color) {
override fun fly() {
super.fly()
}
override fun tweet() {
super.tweet()
}
fun talk(words: String) {
println("$name, the $color parrot, says '$words'")
}
}
다만 상속에는 치명적인 단점이 몇가지 존재합니다.
- 결합도가 높음
- 불필요한 기능 상속
- 부모 클래스와 자식 클래스의 결합으로 인해 동시에 수정해야 함
- 불필요한 인터페이스 상속
- 단일 상속의 한계
다형성(polymorphism)
다형성의 사전적 의미는 하나의 변수, 또는 함수가 상황에 따라 다른 의미로 해석될 수 있는 것을 말합니다.
다형성을 우리가 사용하고 있는 가장 가까운 예시는 overloading을 이용한 print입니다.
@kotlin.internal.InlineOnly
public inline fun println(message: Double) {
System.out.println(message)
}
@kotlin.internal.InlineOnly
public inline fun println(message: CharArray) {
System.out.println(message)
}
@kotlin.internal.InlineOnly
public actual inline fun println() {
System.out.println()
}
위의 코드를 보면 각 함수들은 전부 동일한 이름을 가지고 있습니다. 이러한 함수들은 각각의 매개변수로 서로를 구분합니다. 이렇게 동일한 이름으로 다양한 인자를 받을 수 있게 정의된 방법이 overloading입니다.
추상화 (Abstraciton)
추상화는 복잡한 개념을 숨기기 위해 사용되는 단순한 형식을 의미합니다. 코틀린에서 추상화를 구현하는 가장 쉬운 방법은 인터페이스(interface)를 이용하는 것입니다.
그래서 주로 사용하는 라이브러리를 보면 인터페이스로 표현되어 있는 것을 확인할 수 있습니다. 그렇기 때문에 사용하는 사람들은 내부 코드를 잘 알지못해도 자신의 원하는 형태로 구현할 수 있는 것입니다.
// 추상화를 위한 인터페이스 정의
interface Soundable {
fun makeSound()
}
// 강아지 클래스
class Dog : Soundable {
override fun makeSound() {
println("멍멍!")
}
}
// 고양이 클래스
class Cat : Soundable {
override fun makeSound() {
println("야옹!")
}
}
객체 지향 프로그래밍의 5대 원칙
객체지향에서 꼭 지켜야 할 5개의 원칙을 통틀어 객체 지향 5원칙이라 칭합니다. 이 5개의 원칙의 앞글자를 따서 SOLID라고도 부릅니다.
- Single Responsibility Principle (SRP) - 단일 책임 원칙
- Open-Closed Principle (OCP) - 개방-폐쇄 원칙
- Liskov Substitution Principle (LSP) - 리스코프 치환 원칙
- Interface Segregation Principle (ISP) - 인터페이스 분리 원칙
- Dependency Inversion Principle (DIP) - 의존 역전 원칙
단일 책임 원칙
"객체는 단 하나의 책임만 가져야 한다. "
단일 책임 원칙을 지키기 위해서는 한 클래스가 하나의 기능만 수행하도록 만들어야 합니다.
아래의 예시를 보면 이해가 됩니다.
// SRP 위반
class Employee {
fun calculateSalary() { /* 급여 계산 로직 */ }
fun generateReport() { /* 보고서 생성 로직 */ }
}
// SRP 준수
class Employee {
fun calculateSalary() { /* 급여 계산 로직 */ }
}
class ReportGenerator {
fun generateReport() { /* 보고서 생성 로직 */ }
}
개방-폐쇄 원칙
"확장에는 열려 있으나 수정에는 닫혀 있어야 한다."
개방 폐쇄 원칙을 지키기 위해서는 코드 수정 없이 새로운 기능을 추가할 수 있도록 설계해야 합니다.
// OCP 위반
class Circle(val radius: Double) {
fun calculateArea(): Double {
return Math.PI * radius * radius
}
}
// 원을 그리는 새 모양을 추가하려면 Circle 클래스를 수정해야 함
// OCP 준수
interface Shape {
fun calculateArea(): Double
}
class Circle(val radius: Double) : Shape {
override fun calculateArea(): Double {
return Math.PI * radius * radius
}
}
class Square(val sideLength: Double) : Shape {
override fun calculateArea(): Double {
return sideLength * sideLength
}
}
리스코프 치환 원칙
"자식 클래스는 언제나 부모 클래스를 대체할 수 있다."
리스코프 치환 원칙을 지키기 위해서는 자식 클래스가 부모 클래스와 호환성을 유지해야 합니다.
// LSP 위반
open class Bird {
open fun fly() { /* 날기 로직 */ }
}
class Ostrich : Bird() {
override fun fly() {
throw UnsupportedOperationException("타조는 날 수 없음")
}
}
// LSP 준수
open class Bird {
open fun move() { /* 이동 로직 */ }
}
class Ostrich : Bird() {
override fun move() {
// 날지는 못하지만 이동은 가능함
}
}
인터페이스 분리 원칙
"클라이언트는 자신이 이용하지 않는 메서드에 의존 관계를 맺으면 안 된다."
인터페이스 분리 원칙을 지키기 위해서는 클라이언트가 필요로 하지 않는 메서드를 포함하지 않는 인터페이스를 설계해야 힙니다. 말이 좀 어려운데 결국 자신이 사용하지 않는 메서드에 대한 의존관계를 맺으며 안된다는 것입니다.
// ISP 위반
interface Worker {
fun work()
fun eat()
}
class SuperWorker : Worker {
override fun work() {
// 일하는 로직
}
override fun eat() {
// 밥 먹는 로직
}
}
// ISP 준수
interface Workable {
fun work()
}
interface Eatable {
fun eat()
}
class NormalWorker : Workable, Eatable {
override fun work() {
// 일하는 로직
}
override fun eat() {
// 밥 먹는 로직
}
}
의존 역전 원칙
"추상화에 의존해야지, 구체화에 의존하면 안 된다."
의존 역전 원칙을 지키기 위해서는 추상화에 의존해야 합니다. 그렇게 함으로써 상위 레벨에 클래스가 하위 레벨에 의존적이지 않게 됩니다.
// DIP 위반
class LightBulb {
fun turnOn() {
// 전구를 켜는 로직
}
fun turnOff() {
// 전구를 끄는 로직
}
}
class Switch {
private val bulb = LightBulb()
fun operate() {
bulb.turnOn()
// 스위치가 직접 전구에 의존
}
}
// DIP 준수
interface Switchable {
fun turnOn()
}
class DIPCompliantLightBulb : Switchable {
override fun turnOn() {
// 전구를 켜는 로직
}
}
class DIPCompliantSwitch(private val device: Switchable) {
fun operate() {
device.turnOn()
// 스위치는 추상화된 인터페이스에 의존
}
}
Reference
'기타' 카테고리의 다른 글
CI / CD (0) | 2023.05.29 |
---|---|
DAO, DTO, VO란? (0) | 2023.05.15 |
가독성이 좋은 코드를 작성해야 하는 이유 (0) | 2023.03.25 |
Dependency Injection (DI, 의존성 주입) with Kotlin (0) | 2023.03.25 |