저번에 포스팅했던 SRP에 이어서 OCP를 공부해보려고 한다.
Open-Closed Principle : 개방-폐쇄 원칙
“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”
말이 참 어려운거 같은데 나는 이렇게 정리하고 싶다.
확장에는 열려있다 : 새로운 코드를 추가하여 기능을 확장한다.
변경에는 닫혀있다 : 기능을 확장할 때 원래 코드를 건들이지 않는다.
쉽게 말하면 이미 짜놓은 코드 건들이지 말고 기능을 추가하거나 수정하라는 것 같다.
근데 아무리 생각해도 기능을 추가하고 수정할 때 어떻게 원래 코드를 건들이지 말라는건가 싶다.
그리고 원래 코드의 기준이 뭐지? 라는 생각도 들었다.
그러면 코드를 보면서 이해해보자.
아래 음식 주문을 받고 손님들에게 내어주어야 하는 중국집 프로그램이 있다.
struct ChineseRestaurant {
let orders: [String] = ["짜장면", "짜장면", "짬뽕", "짬뽕", "짜장면"]
func serveOrders() {
for order in orders {
if order == "짜장면" {
print("짜장면 나왔습니다~!")
} else if order == "짬뽕" {
print("짬뽕 나왔습니다~!")
}
}
}
}
ChineseRestaurant().serveOrders()
// result
짜장면 나왔습니다~!
짜장면 나왔습니다~!
짬뽕 나왔습니다~!
짬뽕 나왔습니다~!
짜장면 나왔습니다~!
나는 먼저 이 프로그램의 ‘원래 코드’ 기준을 좀 세워보려고 한다.
간단한 프로그램이지만 중국집의 분명한 목표와 기능을 생각해 봤을 때 주문을 받고 주문에 맞는 음식을 출력하는 것! 그리고 그 목표를 달성하는 것은 ChineseRestaurant이라는 struct이다.
따라서 나는 ChineseRestaurant를 원래 코드라고 생각하겠다.
원래 코드 기준도 세웠으니 직접 기능을 추가하거나 수정해보자.
먼저, 탕수육이라는 메뉴가 추가 되어야 한다고 했을 때, 요구사항에 맞게 코드를 수정해보자.
struct ChineseRestaurant {
// 주문에 탕수육 추가, OCP 위배
let orders: [String] = ["짜장면", "탕수육", "짜장면", "짬뽕", "짬뽕", "짜장면", "탕수육"]
func serveOrders() {
for order in orders {
if order == "짜장면" {
print("짜장면 나왔습니다~!")
} else if order == "짬뽕" {
print("짬뽕 나왔습니다~!")
} else if order == "탕수육" { // 탕수육 로직 추가, OCP 위배
print("탕수육 나왔습니다~!")
}
}
}
}
탕수육이 추가되면서 탕수육 주문에 대한 새로운 로직이 추가되었다.
이 때, 내가 설정한 원래 코드인 ChineseRestaurant를 건들였으므로 우리는 OCP를 위반하게 된 것이다. (변경에 닫혀있어야 한다는 원칙을 위반)
그렇다면, 기능 수정이 필요할 때는 어떨까?
각 메뉴에 로직이 수정되어야 한다고 해보자.
ex) “짜장면 나왔습니다~!” → “맛있는 짜장면 나왔습니다~!”
struct ChineseRestaurant {
// 주문에 탕수육 추가, OCP 위배
let orders: [String] = ["짜장면", "탕수육", "짜장면", "짬뽕", "짬뽕", "짜장면", "탕수육"]
func serveOrders() {
for order in orders {
if order == "짜장면" {
print("맛있는 짜장면 나왔습니다~!") // 로직 수정, OCP 위배
} else if order == "짬뽕" {
print("맛있는 짬뽕 나왔습니다~!") // 로직 수정, OCP 위배
} else if order == "탕수육" {
print("맛있는 탕수육 나왔습니다~!") // 로직 수정, OCP 위배
}
}
}
}
요구사항 변경으로 인해 각 메뉴의 로직이 수정되었다.
역시 내가 설정 해두었던 원래 코드를 건들였으므로 OCP를 위반하였다.
간단한 코드로 예시를 들어서 OCP를 위반한 것이 생각보다 상관없다고 생각할 수 있다.
하지만 실무나 큰 프로젝트에서는 생각보다 문제가 심각해질 수 있다.
프로젝트를 개발하면서 기능 추가와 수정은 필수로 진행되는데 OCP를 위반하며 개발하면 잘 돌아가던 기존 코드들이 먹통이 되는 경우가 분명 발생할 것이다. (실제로 새 기능을 추가할 때 OCP를 위반한 코드는 오류가 꼭 나오드라..)
그렇다면 OCP를 지키면서 중국집 프로그램을 리팩토링 해보자!
일단 OCP를 지키면서 개발하려면 추상화는 기본적으로 깔고 들어가야 한다고 생각해야 한다.
추상화의 기준을 잘 잡고 기존 코드에서 구체타입이 아닌 추상타입을 의존한다면 OCP를 지키며 개발할 수 있다.
나는 order를 추상화하여 이 추상 타입을 의존하도록 하여 리팩토링 하였다. (OrderStrategy를 전략으로 한 전략 패턴 사용)
protocol OrderStrategy { // 주문 전략 프로토콜
func serve()
func canAccept(order: Order) -> Bool
}
struct JajangmyeonOrder: OrderStrategy { // 짜장면 전략
func serve() {
print("짜장면 나왔습니다~!")
}
func canAccept(order: Order) -> Bool {
return order == .jajangmyeon
}
}
struct JjambbongOrder: OrderStrategy { // 짬뽕 전략
func serve() {
print("짬뽕 나왔습니다~!")
}
func canAccept(order: Order) -> Bool {
return order == .jjambbong
}
}
enum Order: String { // 메뉴 enum
case jajangmyeon, jjambbong
}
struct ChineseRestaurant {
let strategies: [OrderStrategy]
let orders: [Order]
init(strategies: [OrderStrategy], orders: [Order]) {
self.strategies = strategies
self.orders = orders
}
func serve(strategy: OrderStrategy) {
strategy.serve()
}
func serveOrders() {
for order in orders {
strategies.filter { $0.canAccept(order: order) }.first?.serve()
}
}
}
// 전략과 주문을 주입
let restaurant = ChineseRestaurant(strategies: [JajangmyeonOrder(), JjambbongOrder()], orders: [.jajangmyeon, .jajangmyeon, .jjambbong, .jjambbong, .jajangmyeon])
restaurant.serveOrders()
// result
짜장면 나왔습니다~!
짜장면 나왔습니다~!
짬뽕 나왔습니다~!
짬뽕 나왔습니다~!
짜장면 나왔습니다~!
전략 패턴을 사용하였지만 너무 신경 쓸 필요 없이 코드가 어떻게 구현 되었는지 보면 된다.
문자열로 받던 주문은 enum타입으로 받았고 OrderStrategy프로토콜을 기반으로 각 메뉴인 짜장면 전략과 짬뽕 전략을 각자 구현해 주었다.
그리고 ChineseRestaurant에서는 단지 전략 프로토콜을 가져오고 주입받은 프로토콜의 serve함수만 반복문으로 수행해주면 끝이다. (canAccept로 order 검증도 진행)
어쨌든 ChineseRestaurant는 짜장면, 짬뽕이라는 구체 타입을 의존하는 것이 아닌 OrderStrategy프로토콜 추상화 타입을 의존한 것을 확인할 수 있다.
그러면 리팩토링 하기 전 코드로 했던 것과 마찬가지로 새로운 기능을 추가하거나 기존 로직을 수정해보자.
먼저, 원래 코드 기준은 똑같이 ChineseRestaurant로 잡으면 될 것 같다.
탕수육 메뉴를 똑같이 추가해보자.
protocol OrderStrategy {
func serve()
func canAccept(order: Order) -> Bool
}
struct JajangmyeonOrder: OrderStrategy {
func serve() {
print("짜장면 나왔습니다~!")
}
func canAccept(order: Order) -> Bool {
return order == .jajangmyeon
}
}
struct JjambbongOrder: OrderStrategy {
func serve() {
print("짬뽕 나왔습니다~!")
}
func canAccept(order: Order) -> Bool {
return order == .jjambbong
}
}
struct SweetSourPork: OrderStrategy { // 탕수육 전략 추가
func serve() {
print("탕수육 나왔습니다~!")
}
func canAccept(order: Order) -> Bool {
return order == .sweetSourPork
}
}
enum Order: String {
case jajangmyeon, jjambbong, sweetSourPork // 탕수육 메뉴 추가
}
// 원래 코드 유지
struct ChineseRestaurant {
let strategies: [OrderStrategy]
let orders: [Order]
init(strategies: [OrderStrategy], orders: [Order]) {
self.strategies = strategies
self.orders = orders
}
func serve(strategy: OrderStrategy) {
strategy.serve()
}
func serveOrders() {
for order in orders {
strategies.filter { $0.canAccept(order: order) }.first?.serve()
}
}
}
먼저 enum타입에 탕수육 메뉴를 추가해주고, OrderStrategy프로토콜을 상속받는 탕수육 전략 구조체를 다른 메뉴와 똑같이 만들어준다.
이러면 탕수육 기능이 추가되었지만 원래 코드인 ChineseRestaurant는 변함이 없는 걸 알 수 있다.
OCP에서 추구했던 “새로운 코드의 추가 만을 통해 새로운 기능을 구현하는 것”을 실현할 수 있다. 또한 원래 코드를 건들이지 않으므로써 “변경에 닫혀 있어야 한다”도 실현할 수 있다.
그렇다면 기존 로직을 수정해야 할 때는 어떨까?
아까와 마찬가지로 serve함수의 print문 로직을 수정한다고 해보자.
protocol OrderStrategy {
func serve()
func canAccept(order: Order) -> Bool
}
struct JajangmyeonOrder: OrderStrategy {
func serve() {
print("맛있는 짜장면 나왔습니다~!") // 로직 수정
}
func canAccept(order: Order) -> Bool {
return order == .jajangmyeon
}
}
struct JjambbongOrder: OrderStrategy {
func serve() {
print("맛있는 짬뽕 나왔습니다~!") // 로직 수정
}
func canAccept(order: Order) -> Bool {
return order == .jjambbong
}
}
struct SweetSourPork: OrderStrategy {
func serve() {
print("맛있는 탕수육 나왔습니다~!") // 로직 수정
}
func canAccept(order: Order) -> Bool {
return order == .sweetSourPork
}
}
enum Order: String {
case jajangmyeon, jjambbong, sweetSourPork
}
// 원래 코드 유지
struct ChineseRestaurant {
let strategies: [OrderStrategy]
let orders: [Order]
init(strategies: [OrderStrategy], orders: [Order]) {
self.strategies = strategies
self.orders = orders
}
func serve(strategy: OrderStrategy) {
strategy.serve()
}
func serveOrders() {
for order in orders {
strategies.filter { $0.canAccept(order: order) }.first?.serve()
}
}
}
역시 로직을 수정하였지만 원래 코드인 ChineseRestaurant는 건들이지 않은 것을 확인할 수 있다.
마찬가지로 원래 코드를 건들이지 않고 로직 수정을 하였으므로 “변경에 닫혀 있어야 한다.”를 지키며 구현할 수 있었다.
이렇게 OCP를 지키며 구현하면 예시처럼 코드의 유지보수성을 향상 시킬 수 있고, 큰 틀인 “원래 코드”를 건들이지 않으므로써 오류의 발생률을 줄일 수 있다.
원래 코드를 건들이지 않고 기능 수정/추가를 할 수 있도록 구현하자!