본문 바로가기

카테고리 없음

[OOP] SOLID (2) 개방 폐쇄 원칙 (Open Closed Principle)

개방 폐쇄 원칙이란

소프트웨어 개체가 변경에는 열려있고, 수정에는 닫혀있어야 한다는 원칙이다. 여기서 변경이란 기능의 추가라 할 수 있고, 변경은 코드의 수정이라 할 수 있다.

 

문장만 보면 와닿지 않으니 예시로 생각해 본다.

개방 폐쇄 원칙을 준수하지 않는다면

1. 실생활 예시

회사에 정수기가 있다. 회사 초기에는 따뜻한 물만 원하는 직원들 때문에 정수기에선 따뜻한 물만 나오고 있었다. 어느덧 회사가 추가 채용을 하여 새로운 직원들이 들어오게 되었다.

 

이때 새로 들어온 직원 중 몇 명은 따뜻한 물은 싫고 시원한 물도 제공해 달라 하여 대표의 지시로 정수기 담당자의 업무가 생겼다. 정수기 담당자는 따뜻한 물이 나오는 출구 옆에 시원한 물이 나오는 출구도 설치하고 해당 출구에는 시원한 물만이 나온다.

그리고 몇 시간 뒤, 기존 직원인 A씨가 평소처럼 따뜻한 물을 먹으러 정수기의 기존 출구에서 물을 받는데 이게 웬걸, 너무나도 미지근한 물이 나오게 된다.

 

이는 정수기 담당자가 시원한 물을 만들기 위해 기존에 나오던 따뜻한 물이 나오던 출구 옆에 파이프를 뚫었다. 그리고 따뜻한 물은 온도를 내리는데 시간이 많이 걸릴걸 예상하여 미지근한 온도로 변경했기 때문이다. 이로써 매일 아침 율무차를 마시던 A씨는 하루의 루틴이 망가져 열불을 내버린다.

 

개방 폐쇄가 무엇을 의미하는지 감이 온다. 개발 세계에서는

[따뜻한 물 : 기존 클래스]
[새로운 직원 : 기획자]
[시원한 물: 요구 사항]
[정수기 담당자 : 기능 추가하던 개발자]

[A씨의 화냄 : 정수기 담당자의 야근]

이 되겠다.

 

정수기 담당자는 무엇을 실수했을까, 시원한 물을 추가하기 위해 기존에 잘만 작동하던 따뜻한 물의 작동에 수정을 가한 것이다. 이를 개방 폐쇄 원칙적으로 수행했다면 시간이 오래 걸릴 것을 예상하여, 더 현실과 맞는 말로는 귀찮더라도, 따뜻한 물이 아닌 애초에 끌어온 순수 물에서 새로운 물길을 내어 변경했어야 한다.

 

개방 폐쇄 원칙을 적용한다면?

 

Open Closed의 의미처럼 정수기의 기능은 확장에 열려있고, 따뜻한 물의 변경은 닫혀있어야 한다.

 

이를 가능케 하는 요소는 추상화와 다형성이 있다. 변경되지 않을 요소(정수기는 송수관을 통해 물을 끌어 올림)과 변경될 요소(어떤 것을 배출할지는 모름)을 추상화하고, 배출될 요소에 대해 다형성을 적용하면 된다.

 

이를 통해 설계를 했다면 적어도 정수기에 얼음이 나오더라도, 물이 포함된 음료가 나오더라도 변화에 있어 유연성 있게 대응할 수 있게 된다.

 

하지만 개발은 현실이다. 느닷없이 정수기에서 에어팟을 나오게 요구할 수도 있다. 그럼 이는 변하지 않을 요소까지 변경되어야 하고 그럼 설계 미스로 이어질까? 미스일 수 있고 아닐 수도 있다. 이전 포스팅인 SRP 원칙에서 말했듯 이는 개발자의 몫이다.

 

이 주제에 대한 레퍼런스를 찾아보면 클래스를 중심으로 설명되어 있다. 나는 개방 폐쇄 원칙이 내포하는 더 본질적인 얘기가 "원본 훼손 금지 원칙"이라 느낀다. 물론 OOP론을 주장했던 창조자들이 "그거 아닌데?"라면 아닌 거지만 그저 느낌이 그렇다.

뇌피셜 - 원본 훼손 금지 원칙

2. 코드 예시

위의 정수기 예시에서도 볼 수 있듯이 결국 담당자는 잘 놀고 있던 원본(따뜻한 물)에 손을 대서 일어난 현상이다. 아마 이와 관련된 요소가 함수형 프로그래밍캡슐화일 것이고 이를 코드로 말해본다.

 

// User.kt 
data class User(
    var name: String,
    var birth: String
)

// Acitivity.kt
private val user: User = User(name="홍길동", birth="2023-02-22")

// 자신의 정보를 보여주는 함수
fun showInfo(user: User) {
    showDialog(user.birth)
}

// 다이얼로그를 보여주는 함수
fun showDialog(user: User) {
    Dialog.Builder
    	.setTitle(user.name)
        .setContent(user.name + "/" + user.birth)
}

// 생일이면 리스트에 보여주는 함수
fun showBirthDayList(user: User) {
    if (today == birth) {
        user.name = "오늘의 주인공 ${name}"
     	showList(user.birth)
    }
}

// RecyclerView.kt
// 리스트에 데이터를 업로드 하는 함수
fun showDialog(user: User) {
    List.Builder
    	.setItem(user.name + "/" + "생일:" + user.birth)
}

 

User 클래스가 있다. Activity 클래스가 User 클래스를 가지고 있고 버튼을 누르면 사용자의 정보가 나타난다. 이는 사용자가 원할 때 반복적으로 띄울 수 있다.

그러다 사용자가 다른 버튼을 눌러 리스트를 보여주는 화면을 띄웠다. 이 클래스는 현재 유저가 생일이면 name에 오늘의 주인공을 덧붙여 리스트에 보여준다.

 

위 코드는 버그가 발생될 여지가 있고 개방 폐쇄 원칙에도 위배된다.

 

사용자가 오늘 생일이면서 리스트 화면에 들어가면 user는 참조 값이기 때문에 원본의 요소가 훼손된다. 그다음 사용자의 정보를 나타내는 버튼을 누르면 이름이 홍길동이 아니라 "오늘의 주인공 홍길동"이 되어 나타난다.

 

흐름상 괜찮은데? 라 느낄 수 있지만 만약 저 데이터가 서버나 DB에 저장된다 생각하면..

 

여기서 위배 요소는 참조 값의 원본 훼손뿐 아니라 전역 변수 또한 한 몫한다. 전역 변수 자체가 어디서든 접근이 가능한데, 이를 수정에 닫혀있게 만들지 않았기 때문이다.

개방 폐쇄 원칙을 적용한다면?

data class User(
    val name: String,
    val birth: String
)

 

코틀린에선 변경될 여지가 없다면 var(variable)보단 val(value)로 선언하는 것을 권장한다. 하지만 현실은 녹록지 않다. 나는 변경할 순간이 올 것 같다. 그보다도 생성자 필드 값의 형태가 Call by Value가 아닌 Call by Reference라면 val 선언은 뚫릴 여지가 있다. 그렇다면?

 

data class User(
    private val _name: String,
    private val _birth: String
) {
    var name: String = ""
    	get() = _name
    var birth: String = ""
    	get() = _birth
    
    fun get(): User = User(_name, _birth)
}

 

위 코드는 name에서 user.name = "오늘의 주인공 ${name}"하더라도 원본을 훼손하지 않는다.

 

추상 클래스를 직접 구현하지 못하고 extends로 구현하는 것처럼, User 클래스 또한 직접 생성자 필드에 접근을 하지 못하고 get() 함수를 통해 접근하고 수정하는 방법이다.

어쩌면 이 것이 전역 변수를 이용하는 상황에서 OCP(개방 폐쇄 원칙)에 가장 가까운 코드라 말할 수 있지 않을까?