선생님, 개발을 잘하고 싶어요.

[프로젝트 삽질기] Observer Pattern, 뭐하는 녀석인데? 본문

개발/소프트웨어 개발

[프로젝트 삽질기] Observer Pattern, 뭐하는 녀석인데?

알고싶은 승민 2020. 1. 10. 01:27

서론

  지금으로부터 1-2년 전, 간단한 이미지 편집, 뷰어 윈도우 프로그램을 만들어야 하는 일이 생겼다. 오늘 포스팅할 내용은 이때의 삽질 경험이다.

오늘의 프로그램

  대략적인 프로그램 구조부터 보고 들어가면 필자의 고민의 흐름을 잘 알 수 있을 거라고 생각된다. 한 번 함께 보자.

디자인 소질은 없는 것 같다.

  크게 프로그램은 3가지 분류로 나눌 수 있다.

1. 이미지를 보여주고 편집하는 이미지부

2. 이미지의 값을 숫자를 사용해서 편집하는 제어부

3. 이미지의 정보를 단순히 보여주는 상태부

 

그리고 오늘 우리가 구현 할 것은 이미지의 배율을 조정하는 것이다. (배율을 조정하는 코드에 관한 이야기는 아니니 부담 없이 읽어 주시길)

 

  배율 조정에 대한 프로그램의 스팩은 다음과 같다.

1. 이미지부에서 휠을 이용해서 배율을 조정할 수 있다.

2. 제어부에서 슬라이더 값을 변경해서 이미지 배율을 조정할 수 있다.

3. 실시간 배율이 상태부에 보여진다.

 

좋다. 좋아. 우리는 프로그램 구조도 알고, 구현해야 하는 기능의 스펙도 아니 코딩을 시작할 수 있겠다. 한 번 시작해보자.

삽질의 시작

  필자는 프로그램의 구조에 따라서 각각 별도의 클래스를 생성하였다.

이미지부를 추상화하는 ImageDepartment

제어부를 추상화 하는 ControlDepartment

상태부를 추상화 하는 StatusDepartment

 

좋다. 우리는 객체지향적으로 생각할 수 있고 완벽한 프로그램을 만들 수 있을 것 같다. (실제 프로그램은 C#으로 만들었으나, 예제는 필자의 선호도에 따라 Kotlin으로 작성했다. 크게 의미는 없다.)

object UIValue { // 편의상, magnitude를 전역 변수로 선언하자.
    var magnitude: Int
}

class ImageDepartment {
    private val image: Image
    
    private fun handleWheelEvent()
    fun updateUI()
}

class ControlDepartment {
    // 배율을 조정할 슬라이더 Control 이다.
    private val slider: Slider
    
    private fun handleSliderEvent()
    fun updateUI()
}

class StatusDepartment {
    fun updateUI()
}

  이미지부, 제어부, 상태부 모두, 전역 변수인 magnitude를 사용해서 화면을 적절하게 그려주는 함수 update()를 가지고 있다. 

 

  또한 이미지부는 사용자의 마우스 휠 이벤트를 적절히 처리하는 함수를 가지고 있고

  제어부는 사용자의 슬라이드 이벤트를 적절히 처리하는 함수를 가지고 있다.

 

  상세한 구현을 한 단계 들어가면 다음과 같은데

class ImageDepartment {
    ...
    
    private fun handleWheelEvent(diff: Int) {
        UIValue.magnitude += diff
        updateUI()
    }
    
    fun updateUI() {
        image.magnitude = UIValue.magnitude
    }
    ...
}

  이는 사용자의 이벤트로 프로그램의 상태를 갱신시키고, 갱신된 상태에 따라 화면을 다시 그리는 코드를 명시적으로 작성해 준 것이다. 문제가 없어 보인다. 나머지 클래스들도 마찬가지로 구현을 할 수 있다.

하지만 걔네들은 전부 하나라구요!

  사실 위와 같은 구현은 하나의 Department에서는 정확하게 동작한다. 하지만 여러 개의 Department가 하나의 공통 변수를 수정하고 있기 때문에 이 프로그램은

 

1. 이미지를 마우스 휠로 드래그해서 배율을 맞춰놨지만 제어부의 슬라이더와 상태부의 값은 그대로 예전 배율을 보여준다.

2. 제어부의 슬라이더를 드래그 해서 배율을 변경했지만, 실제 이미지의 배율은 변경이 안되어 있다.

 

  왜 그런고 하니, 이미지부가 전역 변수를 수정하고, 수정을 반영하는 함수를 본인만 호출했기 때문이다. 오 젠장, 다시 코드를 작성하자.

class ImageDepartment {
    ...
    
    private var controlDepartment: ControlDepartment? = null
    private var statusDepartment: StatusDepartment? = null
    
    private fun handleWheelEvent(diff: Int) {
        UIValue.magnitude += diff
        updateUI()
        controlDepartment?.updateUI()
        statusDepartment?.updateUI()
    }
    
    fun updateUI() {
        image.magnitude = UIValue.magnitude
    }
    
    fun setControlDepartment(cd: ControlDepartment) {
        controlDepartment = cd
    }
    
    fun setStatusDepartment(sd: StatusDepartment) {
        statusDepartment = sd
    }
    
    ...
}

  흠... 이제 이미지부에서 마우스 휠을 할 경우, 그 변경 사항을 제어부도 상태부도 알고 적절하게 값을 변경할 것이다. 정상적으로 동작하니까 만족하나? 흠... 뭔가 아니다. 제어부의 구현을 볼 차례다.

class ControlDepartment {
    ...
    
    private var imageDepartment: ImageDepartment? = null
    private var statusDepartment: StatusDepartment? = null
    
    private fun handleSliderEvent() {
        UIValue.magnitude += slider.magnitude
        updateUI()
        imageDepartment?.updateUI()
        statusDepartment?.updateUI()
    }
    
    fun updateUI() {
        slider.magnitude = UIValue.magnitude
    }
    
    fun setImageDepartment(id: ImageDepartment) {
        controlDepartment = id
    }
    
    fun setStatusDepartment(sd: StatusDepartment) {
        statusDepartment = sd
    }
    
    ...
}

  그래! 이제 문제가 잘 보인다. 도대체 이미지부도 제어부를 참조하고, 제어부도 이미지부를 참조하는데, 도대체 어느 순간에 서로가 서로를 알 수 있을까? 정책상으로 결정이 될 것이다. 이미지부에서 setControlDepartment를 호출하는 순간, 해당 제어부에 본인의 참조를 넘기도록 할 수 있을 것이다.

서로가 서로를 알아야만 하는 이 복잡 다단함

 

  하... 한숨이 나온다. 이 상태에서 기능 스펙이 바뀐다고 상상해보자. 정보를 실시간으로 보여주는 소규모 패널이 추가되었다고 생각해보라 도대체 이 코드의 어디를 수정해야 할까?

  • magnitude를 수정할 수 있는 모든 코드를 변경해야 한다. (새로 추가된 패널의 update 코드를 실행하도록 변경해야 한다.)
  • magnitude를 수정하는 모든 클래스가, 새로 생긴 부서의 참조를 받아올 방법을 고안해야 한다.

  이제 문제가 좀 다르게 보이나? 스스로를 자학하는 코드를 작성한 것이다. 뭔가 다른 게 필요하다. 살려줘.

Observer Pattern 이 널 구하리라

  오늘의 프로그램의 예는 

본질이 되는 변수 하나가 있고, 다양한 클래스에서 그 변수가 수정되는 경우 적절한 처리를 해줄 필요가 있는 상태이다.

  그리고 우리의 자애로운 GoF는 이런 문제 상황을 해결한 방법을 [디자인 패턴 정리 - GOF]에 작성해 놓았다. 한시름 놓았다. 숨을 고르고 리팩토링을 해보자.

 

  우선 이런 문제 상황을 해결하는 디자인 패턴은 바로 Observer Pattern이다. 일단 클래스 다이어그램을 띄워둘 것인데, 그냥 그러려니 넘겨도 된다. 하나하나 의도를 설명하고 실제 프로젝트에 적용해 볼 예정이니 걱정하지 마라.

 

와! 싸이클!

 

그게 뭔데 이 녀석아

  여러분들은 잡지를 구독해본 경험이 있을 것이다. 혹은 신문 구독, 편지 구독도 마찬가지다. 요즘 문물로 따지면 유튜브 구독도 마찬가지 맥락으로 이해할 수 있다.

 

  나와 다른 친구 한 명이 잡지를 구독했다고 생각해보자. 

뭐 대략 요런 상태다. 내가 잡지사에 구독을 요청했다.

  잡지사는 구독 신청이 들어오면 명부에 나와 내 친구를 적고 잡지를 열심히 만들 것이다. 그리고 5월의 잡지가 발행될 때, 무책임하게 각자의 집 앞에 5월 잡지를 각자의 집으로 배송할 것이다.

 

  그리고 우리는 집 앞에 배송된 잡지를 들고 와 신나게 잡지를 보면 된다. 쉽지 않은가? 그리고 가만히 생각해보면 우리가 원하는 것도 이런 것이다. 다시 그림에 이름표를 붙여 보자.

 

 

좀더 우리의 프로그램처럼 만들어봤다.

  이제 각 부서들은, 서로가 서로를 알 필요가 없다. Magnitude를 구독만 하면, 값이 변경됨에 따라 적절한 UI 처리가 가능하고 심지어 매우 동시적으로 일어난다.

 

동작하는 코드를 작성해보자.

  우리는 크게 두 가지의 인터페이스를 생각할 수 있다.

1. Observer : 관심 주제를 구독하는 주체다. (위의 예에서 나, 나의 친구)

2. Subject : 실제 본질 데이터를 관리하고 구독자에게 데이터 변경을 알려준다. (위의 예에서 잡지사 - 참고로 잡지사는 우리 집 앞으로 배송하는 책임도 있다. RG?)

 

  우리는 프로그램을 다음처럼 바꿀 것이다.

object UIValue {
    // magnitude는 그냥 값이 아니고, 구독자에게 자신의 변경을 알리는 Subject가 되었다.
    var magnitude: Subject<Int>
}

class ImageDepartment {
    ...
    
    private val magniObserver = object : Observer<Int> {
        override fun update(newValue: Int) {
            // magnitude 가 변경될 때 자동으로 실행되는 함수를 선언한다.
            image.magnitude = UIValue.magnitude
        }
    }
    
    private initUI() {
        // 구독 신청 하자
        UIValue.magnitude.subscribe(magniObserver)
    }
    
    private fun handleWheelEvent(diff: Int) {
        // magnitude 에 새로운 값을 발행한다.
        // 그렇다. 잡지사의 예제와 다른 부분이 이 곳인데, 객체는 구독자이자 발행하는 사람일 수 있다.
        UIValue.magnitude.value = (UIValue.magnitude.value + diff)
    }
    
    ...
}

  코드와 실제 예를 좀 비교하며 설명해보자.

 

  • Subject는 잡지사이다. 잡지가 발행되면, 모든 구독자에게 잡지를 보내준다. 프로그램에서 Int 타입인 Magnitude를 관리한다.
  • Observer의 update 함수는 우리가 잡지가 도착하면 하는 행동을 작성하는 곳이다. 프로그램에서 Magnitude가 변경될 때, 화면을 갱신하는 행위가 이에 해당한다.
  • 독자는 잡지사에 구독 신청을 해야 한다. 이를 하고 있는 것이 ImageDepartment.initUI()의 행동이다.
  • 여기가 실제랑 조금 차이가 있는데, 프로그램에서는 잡지를 발행 담당이 따로 없다. 모두가 발행자가 될 수 있다. 새로운 잡지를 발행하는 코드는 ImageDepartment.handleWheelEvent()이다.

  이미지부는 더 이상 다른 부서의 참조를 가지고 있을 필요가 없다. (심지어 미래에 만들어질 무한한 부서의 참조도 걱정할 필요가 없다!) 단지 값을 발행하고, 구독해서 받아온 값으로 적절한 처리를 하면 된다.

 

  이제, Observer와 Subject의 실제 구현을 보자.

class <T> Subject<T> {
   private val observers = mutableListOf<Observer<T>>()

   var value: T
       set (newValue) {
           field = newValue 
           // 값이 변경 될 때, 모든 구독자에게 값을 발행한다.
           for (ob in observers) {
               ob.update(newValue)
           }
       }
       
   fun subscribe(observer : Observer<T>) {
       observers.add(observer)
   }
}

// 인터페이스로 Observer 를 구현해 놓았다.
interface <T> Observer<T> {
   fun update(newValue: T)
}

  정말 간단한 구현이다. Observer interface는 단순히 값을 이용해 업데이트를 할 인터페이스만 제공하는 형태이다. 주요 구현체는 Subject이고 잠깐만 설명해보자.

 

  • 내부적으로 Observer 리스트를 가지고 있다. (잡지사가 구독자의 정보를 가지고 있는 것이다.)
  • value는 세팅될 때, 모든 Observer에게 새로운 값을 알려준다. (잡지사가 구독자에게 잡지를 배송하는 것이다.)
  • subscribe를 통해, 새로운 Observer의 구독을 등록한다. (잡지사가 구독자를 명부에 적는 것이다.)

결론

  Observer Pattern을 사용해서 좀 더 응답성이 좋고 나중에 새로운 기능 추가 시에도 유연하게 대응할 수 있는 능력을 얻었다.

 

  객체 지향은 관심사의 분리가 가장 중요한데, Observer Pattern은 관심사 분리의 끝판 왕이라고 생각한다.

  본질이 되는 데이터를 하나를 별도로 두고, 해당 객체에 관심 있는 객체들만 선택적으로 구독한다는 개념은 재밌다.

 

  그리고 구독자 간의 연관 관계를 끊어버리는 것이 가장 큰 장점이라고 생각한다. (실제로 오늘의 예시에서 본 참조 지옥에서 벗어나는데 일조했다.)

 

  더 나아가서 예제에서는 구독하는 Subject가 특정한 값이었지만, 이를 무한히 확장할 수 있다. 예를 들어 사용자의 이벤트를 구독하는 것이 가능하겠다, 버튼을 누르는 것을 생각하면 이해하기 쉽다.

  이러한 개념을 더욱 확장하면 선언형 프로그래밍, Reactive Programming의 시작이 된다. 추후에 RP에 대한 개념 도입도 포스팅으로 남겨야겠다.

 

  사실 RP 포스팅 준비하다가, 예전 생각나서 쓴 포스팅인 건 안 비밀이다.

Comments