객체 지향 프로그래밍을 설계할 때는 SOLID 원칙
을 지켜야 한다.
S ingle Responsibility Principle(SRP)
O pen Closed Principle(OCP)
L iskov Substitution Principle(LSP)
I nterface Segregation Principle(ISP)
D ependency Inversion Principle(DIP)
SRP(Single Responsibility Principle, 단일 책임 원칙)
모든 클래스는 단 하나의 책임만 가져야 하고 메서드나 클래스를 수정하는 이유도 단 하나여야 한다는 원칙이다.
예를 들어, 당근 마켓에서 중고거래를 하는 경우를 생각해보자.
당근 마켓의 유저는 크게 판매자와 구매자로 나눌 수 있다.
// SRP를 준수하지 않은 코드
class Daangn {
fun searchProducts() {
// 구매자가 상품 검색
}
fun dibProducts() {
// 구매자가 상품 찜
}
fun registerProducts() {
// 판매자가 상품 등록
}
fun changeProductState() {
// 상품의 판매 상태 변경
}
fun makeReservation() {
// 판매자가 거래 약속 설정
}
fun sendDealReview() {
// 거래후기 전송
}
}
Daangn
이라는 클래스 안에 구매자와 판매자의 액션이 전부 포함되어있다. 따라서 이 클래스는 크게 두 가지의 책임을 지고 있는 셈이다.
거래 후기를 남기는 것 이외에는 판매자와 구매자의 역할은 명확하게 구분되어있기 때문에 한 클래스에 묶어둘 필요가 없다.
따라서 아래와 같이 클래스를 분리하면 Buyer
는 구매자의 책임만, Seller
는 판매자의 책임만 갖게 되는 것이다.
class Buyer {
fun searchProducts() {
// 구매자가 상품 검색
}
fun dibProducts() {
// 구매자가 상품 찜
}
}
class Seller {
fun changeProductState() {
// 상품의 판매 상태 변경
}
fun makeReservation() {
// 판매자가 거래 약속 설정
}
}
SRP의 장점
SRP를 준수할 경우 우선 코드만 봐도 이해하기 쉽다. 장황했던 하나의 클래스가 짧게 분리되었기 때문이다. 따라서 유지보수도 더욱 쉬워진다.
또한, 판매자의 역할을 수정하고 싶은 경우 Seller 클래스의 코드만 보면 되고 Buyer 클래스를 볼 필요가 없기 때문에 관리가 쉬워진다.
OCP(Open Closed Principle, 개방 폐쇄 원칙)
이름만 보면 뭔가 열린 교회 닫힘 이런 느낌 ㅎㅎ
OCP는 확장에 대해서는 열려있고, 수정에 대해서는 닫혀있어야 한다는 원칙이다.
말이 조금 어렵지만 기존의 코드를 최대한 건드리지 않으면서 원하는 기능은 쉽게 추가할 수 있어야 한다는 뜻.
즉, 수정 없이도 확장 가능한 코드여야 한다는 의미이다.
class Daangn {
fun sendDealReview() {
// 거래후기 전송
}
}
위와 같은 Daagn 클래스에 구매자의 액션을 추가하고 싶다고 할 때
open class Daangn {
fun sendDealReview() {
// 거래후기 전송
}
}
class Buyer : Daangn() {
fun searchProducts() {
// 구매자가 상품 검색
}
fun dibProducts() {
// 구매자가 상품 찜
}
}
이처럼 하면 Daagn 클래스를 수정하지 않고도 Daagn 클래스의 기능에 새로운 기능을 추가할 수 있다.
LSP(Liskov Substitution Principle, 리스코프 치환 원칙)
프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙이다.
open class Rectangle {
fun setWidth(width: Int) { ... }
fun setHeight(height: Int) { ... }
}
internal class Square : Rectangle() {
...
}
위의 코드는 LSP를 준수하지 않은 코드이다. 왜일까?
// 가능
val r = Rectangle()
r.setWidth(5)
r.setHeight(7)
// 불가능
val r = Square()
r.setWidth(5)
r.setHeight(7)
Square 클래스는 Rectangle 클래스를 상속받고 있기 때문에 LSP의 원칙대로라면 val r = Squre()
이라고 했을 때도 문제가 없어야 한다.
하지만 이렇게 하면 가로와 세로의 길이가 동일해야 한다는 Square의 특성을 위반하기 때문에 처음의 코드가 LSP를 준수하지 않은 것이다.
ISP(Interface Segregation Principle, 인터페이스 분리 원칙)
인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다는 원칙이다.
클라이언트는 자신이 사용하지도 않을 인터페이스에 의존하면 안 된다.
아래의 코드를 보자.
interface Printer {
fun print()
fun fax()
fun scan()
}
internal class EconomicPrinter : Printer {
override fun print() {}
override fun fax() {
throw NotSupportedException()
}
override fun scan() {
throw NotSupportedException()
}
}
저가형 프린터의 경우 팩스와 스캔의 기능은 갖고 있지 않다. 하지만 Printer
라는 인터페이스를 상속받으면서 팩스와 스캔 함수까지 불필요하게 구현해야 한다.
따라서 위의 코드를 ISP원칙을 준수하여 수정하면 아래와 같이 변경할 수 있다.
interface Printer {
fun print()
}
interface Fax {
fun fax()
}
interface Scan {
fun scan()
}
internal class EconomicPrinter : Printer {
override fun print() {}
}
DIP(Dependency Inversion Principle, 의존 역전 원칙)
클래스는 구체적인 클래스가 아니라 추상 클래스에 의존해야 한다는 원칙이다.
1) DIP를 준수하지 않은 코드
class Portfolio {
private val exchange: TokyoStockExchange = TokyoStockExchange()
fun value(): Money {
val total = Money(0)
for (item in itemList) total.add(exchange.currentPrice(item))
return total
}
}
Portfolio 클래스 내부에서 TokyoStockExchange
인스턴스를 생성하고 이것의 현제 시세를 구하고 있기 때문에 Portfolio 클래스는 TokyoStockExchange 클래스에 의존하고 있다.
하지만, 여기서 도쿄가 아니라 뉴욕이나 한국의 증권 거래소를 이용하고 싶다면 어떻게 해야 할까?
아마 Portfolio 클래스 내부를 대폭 수정해야 할 것이다.
2) DIP를 준수한 코드
interface StockExchange {
fun currentPrice(symbol: String?): Money
}
class TokyoStockExchange : StockExchange {
override fun currentPrice(symbol: String?): Money {
// TokyoStock-specific code
}
}
class NewYorkStockExchange : StockExchange {
override fun currentPrice(symbol: String?): Money {
// NewYorkStock-specific code
}
}
class KoreaStockExchange : StockExchange {
override fun currentPrice(symbol: String?): Money {
// KoreaStock-specific code
}
}
// exchange를 interface인 StockExchange 타입으로 선언함으로써 exchange에 누구든 들어올 수 있음
class Portfolio(private val exchange: StockExchange) {
fun value(): Money {
val total = Money(0)
for (item in itemList) total.add(exchange.currentPrice(item)) // exchange에 어떤 타입이 들어오던 수정될 일 없음
return total
}
}
StockExchange
인터페이스는 현재 가격을 묻는 개념을 추상화하고 있다.
이제 Portfolio 클래스가 TokyoStockExchange 클래스가 아닌, StockExchange 라는 인터페이스에 의존하고 있기 때문에 DIP 원칙을 준수하고 있다고 할 수 있고
따라서 우리가 어느 나라의 증권 거래소를 이용하고 싶던지 간에 StockExchange에 다른 인자를 넣어 Portfolio 클래스를 사용하면 되고 value 함수도 수정될 일이 없다.
'CS > 개발상식' 카테고리의 다른 글
TDD(Test-Driven Development) (0) | 2022.09.15 |
---|---|
RESTful API (0) | 2022.09.09 |
오버로딩(Overloading) vs 오버라이딩(Overriding) (0) | 2022.09.07 |
객체 지향 프로그래밍(OOP, Object-Oriented Programming) (0) | 2022.09.05 |
좋은 코드란 무엇인가 (0) | 2022.08.31 |