디자인 패턴

데코레이터 패턴(Decorator Pattern)

늦깎이_개발자 2024. 6. 24. 23:13
반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

데코레이터 패턴(Decorator Pattern)은 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴이다. 데코레이터 패턴은 컴포지션(composition)과 위임(delegation)을 사용하여 원래 객체의 인터페이스를 유지하면서 기능을 추가하는 방법이다.

 

주요 구성 요소는 다음과 같다.

  • 컴포넌트(Component) : 기본 인터페이스나 추상 클래스로, 구체적인 기능을 정의한다. 데코레이터와 구체적인 컴포넌트 객체 모두 이 컴포넌트를 구현한다.
  • 구체적인 컴포넌트(Concrete Component) : 컴포넌트 인터페이스를 구현한 클래스이다. 컴포넌트 인터페이스에서 정의된 기능을 구현한다.
  • 데코레이터(Decorator) : 컴포넌트 인터페이스를 구현하거나 확장한 추상 클래스이다. 이 클래스는 컴포넌트 객체를 포함하며, 컴포넌트의 메서드를 호출(위임)하는 메서드를 갖는다.
  • 구체적인 데코레이터(Concrete Decorator) : 데코레이터를 확장한 클래스이다. 컴포넌트 인터페이스에서 정의된 기능을 구현함으로써 동적으로 추가될 기능을 구현한다.

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.

 

아래는 예제 소스 코드의 클래스 다이어그램이다.

FileOut은 컴포넌트 인터페이스에 해당한다. 문자열(String)의 내용을 파일에 쓰는 기능을 정의한다. FileOutImpl은 구체적인 컴포넌트에 해당한다. 실제로 파일에 쓰는 기능을 구현한다.

 

아래는 Decorator 클래스의 코드이다.

abstract class Decorator(
    private val delegate: FileOut
) : FileOut {
    fun doDelegate(data: String) {
        delegate.write(data)
    }
}

데코레이터 클래스는 컴포넌트 인터페이스에 해당하는 FileOut을 구현함과 동시에, FileOut 객체를 주입받는다. 그리고 주입받은 객체에 기능 실행을 위임하는 doDelegate() 메서드를 가진다.

 

다음은 데코레이터 클래스를 확장한 구체적인 데코레이터 클래스의 예시이다.

class EncryptionOut(
    delegate: FileOut
) : Decorator(delegate) {
    override fun write(data: String) {
        val encryptedData = encrypt(data)
        super.doDelegate(encryptedData)
    }

    private fun encrypt(data: String): String {
        println("data is encrypted.")
        return "encrypted $data"
    }
}

구체 클래스인 EncryptionOut은 파일의 내용을 암호화하는 기능을 가졌다고 가정해보자. 위 코드를 보면, input으로 들어온 data를 암호화하고 그 결과값을 데코레이터의 doDelegate메서드에 전달함으로써, 주입받은 FileOut 객체에 기능 실행을 위임한다.

 

이제 클라이언트 코드에서 데코레이터 패턴을 사용함으로써, 동적으로 객체의 기능을 추가할 수 있다. 아래 예시를 살펴보자.

class DecoratorTest {

    @Test
    fun write() {
        // given
        val fileOut = FileOutImpl() // 일반적인 파일 쓰기 기능
        val encryptionOut = EncryptionOut(fileOut) // EncryptionOut 데코레이터를 적용함으로써 암호화 기능을 추가
        
        // when, then
        assertDoesNotThrow { encryptionOut.write("data") }

        // given
        val zipEncryptionOut = ZipOut(EncryptionOut(fileOut)) // ZipOut 데코레이터를 적용함으로써 압축 기능을 추가

        // when, then
        assertDoesNotThrow { zipEncryptionOut.write("data") }

        // given
        val encryptionZipOut = EncryptionOut(ZipOut(fileOut)) // EncryptionOut, ZipOut을 순차적으로 적용

        // when, then
        assertDoesNotThrow { encryptionZipOut.write("data") }
    }
}

 

우리가 흔히 접할 수 있는 코드 중 데코레이터 패턴이 적용된 대표적인 예는 스프링 프레임워크의 트랜잭션 처리이다. @Transactional 어노테이션을 응용 서비스 클래스에 붙이면, 트랜잭션 기능이 추가된 데코레이터 객체(프록시 객체)를 런타임에 생성하여 주입한다.

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형