- 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙(SRP, OCP, LSP, ISP, DIP)
- SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.
단일 책임 원칙 (SRP : Single Responsibility Principle)
- 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙
- 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는 데 집중되도록 클래스를 따로따로 여러 개 설계하라는 것
- 책임의 범위는 딱 정해져있는 것이 아니고, 어떤 프로그램을 개발하느냐에 따라 개발자마다 생각 기준이 달라질 수 있다.
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) {}
}
개방 폐쇄 원칙 (OCP : Open Closed Principle)
- 클래스는 확장에 열려있어야 하며, 수정에는 닫혀있어야 한다는 원칙
- 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화하도록 프로그램을 작성해야 하는 설계 기법
- 확장에 열려있다 : 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있음.
- 변경에 닫혀있다 : 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한함.
- 추상화 사용을 통한 관계 구축을 권장을 의미하는 것
- 즉, 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적인 설계 원칙
BEFORE
- 문제점 : 고양이와 개 외에 양이나 사자를 추가하면, 각 객체의 필드 변수에 맞게 when문을 분기하여 구성해줘야 한다.
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) // 어흥
}
리스코프 치환 원칙 (LSP : Liskov Substitution Principle)
- 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다는 원칙
- 다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미하는 것
BEFORE
- 문제점 : Fish 물고기 클래스에 Animal 추상 클래스를 상속하면, 물고기는 행할 수 없는 speak() 메서드를 구현해야 한다.
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("물고기가 헤엄치다.")
}
}
인터페이스 분리 원칙 (ISP : Interface Segregation Principle)
- 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 원칙
- SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것
- 즉, SRP 원칙의 목표는 클래스 분리를 통하여 이루어진다면, ISP 원칙은 인터페이스 분리를 통해 설계하는 원칙
- 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표
- 한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스를 분리하는 행위를 가지지 말아야 한다.
BEFORE
- 문제점 : 최신 기종 스마트폰 뿐만 아니라 구형 기종 스마트폰 클래스도 다뤄야 할 상황처럼 갤럭시 S3 클래스를 구현해야 한다면 무선 충전, 생체인식과 같은 기능은 포함되어 있지 않다.
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) { /*...*/ }
}
의존 역전 원칙 (DIP : Dependency Inversion Principle)
- 어떤 클래스를 참조해서 사용해야 하는 상황이 생긴다면, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 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")
}
}
'안드로이드 > Kotlin' 카테고리의 다른 글
[Java] 읽기 전용 컬렉션(List, Set, Map), ArrayList, LinkedList, Array (0) | 2024.12.05 |
---|---|
[Kotlin] 변수 선언(val, var, const val) (0) | 2024.12.05 |
[Kotlin] 자료형(기본형, 참조형), Call by Value, Call by Reference (0) | 2024.12.05 |