본문 바로가기

웹개발 - Back 관련/Architecture

[ GOF ] 반복자 패턴 - Iterator Pattern with Kotlin

Topic =  반복자 패턴의 의도와 Kotlin 사용 방법 알아보기.


Purpose

  • Iterator 인터페이스를 추상화 함으로 Array, List, Map 등 컬렉션 모두 동일한 방식으로 순회할 수 있도록 한다.
  • 반복문의 요소를 순회하는 방법을 캡슐화 하여 사용자는 각 요소에 대해 수행할 작업만 정의하면 된다.

 

예시로 비교해보면서 학습하는 반복자 패턴 개념

 

[ 반복자 패턴을 사용하지 않는 예시 ]

val myArr = arrayOf(1,2,3,4,5)
val myList = arrOf(1,2,3,4,5)

for (i in 0 until myArr.size ) {
    println(myArr[i]) <-- A[i] 재사용하기 어려움
}

for (i in 0 until myList.size) {
    println(myList[i]) <-- B[i] 재사용하기 어려움.
}
  • 특정 배열의 인덱스를 직접 접근하여 반복하는 방식으로, 직접 상태 변수 i를 관리한다,
  • 배열 순회 방식의 구현과, 접근을 처리하는 부분이 결합되어 있다.

 

[ 반복자 패턴을 사용하는 예시 ]

val myList = listOf(1,2,3,4,5)
val myArr = arrayOf(1,2,3,4,5)

myList.forEach {
    println(it) <-- 직접 인덱스 i를 i++하면서 관리할 필요가 없어짐.
}

myList.maxBy { it } <-- 상태 관리 로직 필요 X
myArr.maxBy { it } <--  상태 관리 로직 필요 X 및 객체 집합에 대한 접근이 일관성 있다.

 

forEach은 내부 로직이 드러나진 않지만, 인덱스가 0부터 시작하는 것은 for(i in n .. m) 와 동일한데, 어떤 차이가 있나.

  • 먼저, forEach는 내부에서 Iterator 인터페이스를 구현하여서 작동하며, 여러 컬렉션형태에서도 동일하게 사용이 가능하며, 가장 큰 차이점은 반복문 사용 시점에 상태변수 i 를 i++ 해가면서 직접 컨트롤할 필요가 없어졌다.

 

[ maxOf {} 소스코드 ]

public fun Iterable<Double>.max(): Double {
    val iterator = iterator()
    if (!iterator.hasNext()) throw NoSuchElementException()
    var max = iterator.next()
    while (iterator.hasNext()) {
        val e = iterator.next()
        max = maxOf(max, e)
    }
    return max
}

fun main {
    fun testA() {
    	listOf(1,2,3,4,5).max()
    }
}
  • 추가로 maxOf {} 소스코드를 확인해보면, 사용자는 상태변수 max를 관리할 필요가 없이 캡슐화된 로직을 통해 간편하게 요소 중 제일 큰 값을 얻을 수 있다.

 

🌟반복자 패턴을 알기 전에도 자연스럽게 java, Kotlin 에서 제공하는 forEach, map, maxOf, sumOf, reduce 등 다양한 반복자 패턴이 적용된 반복문을 사용해 왔다. 때문에 비즈니스 로직에서 반복문을 순회하며 람다로 특정 로직을 실행할 수 있는 forEach 기능을 별도로 작성하지 않고 사용할 수 있고, 컬렉션 요소의 합계를 구하는 로직을 별도로 정의할 필요 없이 sumOf 메서드를 통해 합계를 구할 수 있었다. Iterator 인터페이스를 추상화하여 어느 타입의 컬렉션이 오더라도 동일한 결과를 얻을 수 있게 된 것이다.

 

[ 반복자 패턴을 직접 생성하는 방법 ]

➡️ 구현과 접근의 분리

forEach와, maxOf 등 java, kotlin에서 기본적으로 제공하는 구현 로직을 변경할 수 없기 때문에, 변동, 확장 가능성이 큰 반복문 로직은 직접 반복자 패턴을 따라 만들어서 사용할 수 있다. 즉, 100개의 반복문에 적용된 구현로직을 변경함으로 100개의 반복문을 수정할 필요가 없어진다.

 

data class Student(
    val name : String,
    val age: Int,
    val birth: Date,
)

 

  • 사용할 객체를 간단하게 작성

 

[ Aggregate 집합체 역할,  ConcreteAggregate 구체적인 집합체 역할 정의 ]

class StudyRoom : Iterable<Student> {

    private val students = mutableListOf<Student>()
    private var size = 0

    fun addStudent(name: String, age:Int, birth: Date){
        students.add(Student(name,age,birth))
        size++
    }

    fun getSize(): Int {
        return size
    }

    fun getStudent(index: Int): Student {
        return students[index]
    }

    override fun iterator(): Iterator<Student> {
        return StudentIterator(this)
    }
}
  • 단순 이해
    • override fun iterator() 메서드를 통해 Iterator를 만들어 내는 인터페이스를 결정한다. 즉, 내가( StudyRoom )갖고 있는 students를 반복문을 돌려줄 객체를 지정하는 것이다.
  • 개념적 관점
    • ➡️ Iterable 집합체 구현을 통해 StudyRoom이라는 구체적인 잡합체를 만드는 것으로, Iterator를 구현하는 구체적인 반복자를 통해 집합체를 반복할 수 있게 지정해주는 것이다.

 

[ Iterator 반복자 역할, Concrete Iterator 구체적 반복자 역할 정의 ]

class StudentIterator(private val target: StudyRoom ) : Iterator<Student> {

    private var index = 0

    override fun hasNext(): Boolean {
        return index < target.getSize()
    }

    override fun next(): Student {
        return if (!hasNext()) throw NoSuchElementException() else target.getStudent(index++)
    }
}
  • 단순 이해
    • 프로퍼티로 전달 받은 target에 대한 반복문 처리 로직을 담당하는 Iterator 클래스를 분리해서 정의한다. 만약, 여기서 인덱스를 target의 컬렉션 요소의 사이즈로 해두고, next()에서 index를 -- 하면서 downTo 와 같은 로직으로 집합체에 대한 반복문을 변경할 수도 있는 것이다.
  • 개념적 관점
    • ➡️추상화된 Iterator 반복자를 StudentIterator 에서 next(), hasNext() 메서드를 통해 요소를 순회하도록 작성하는 구체적인 반복자로 구현한다. 

 

[ 정의된 구현체 접근 ]

private val logger = KotlinLogging.logger {  }
fun main {

    private val room1 = StudyRoom()

    fun iteratorTest(){
        room1.addStudent("test1",7, Date.valueOf(LocalDate.of(2011,1,1)))
        room1.addStudent("test2",18, Date.valueOf(LocalDate.of(2000,1,1)))
        room1.addStudent("test3",14, Date.valueOf(LocalDate.of(2004,1,1)))

        // 객체, map, list 모두 일관된 접근 방식을 제공하며, 상태관리 직접 X
        val iteratorObject = room1
        iteratorObject.iterator().printIterator()

        val iteratorMap = mapOf(1 to "test1", 2 to "test2", 3 to "test3")
        iteratorMap.iterator().printIterator()

        val iteratorList = listOf("test1","test2","test3")
        iteratorList.iterator().printIterator()

        // 람다형식으로 만들기
        iteratorList.iterator().returnIterator { it ->
            logger.info { it }
        }
    }
}

fun <T> Iterator<T>.printIterator (){
    var itr = this
    while (itr.hasNext()) {
        when (val stu = itr.next()) {
            is Student -> { logger.info { "Student name : ${stu.name}, age : ${stu.age}, birth : ${stu.birth}" }}
            is Map.Entry<*,* > -> { logger.info { "key - ${stu.key}, value - ${stu.value}" }}
            else -> { logger.info { "value : $stu" }}
        }
    }
}

 

  • 예제와 같이 Iterator.xxx 확장함수 내부에 작동로직을 작성할 수도 있고, 람다 함수 정의를 통해서 forEach와 같이 반복마다 어떤 작동을 할 것인지 선택할 수 있도록 할 수도 있다.
  • 이제 printIterator() 메서드를 통해 Map, List, Array 등 다양한 컬렉션에 동일하게 작동되는 반복문을 작성할 수 있게 되었다. - purpose1.
  • 반복자패턴을 적용하지않고 StudyRoomA = List<Student>() 와 같이 여러 리스트로 관리하며 예시와 같이 학생의 Age 순서대로 반복을 돌리던 중, Age의 최빈값을 구하여 최빈값으로 부터 편차가 작은 순서대로 요소를 순환하는 로직으로 변경할 경우 모든 반복문을 수정해야한다. 위와 같이 작성하게 되면, 반복자를 수정할 경우에 해당 반복자를 의존하는 집합체들의 로직과 집합체를 사용하는 접근 로직에 변동 없이 반복문을 수정할 수 있게 된다. - purpose2

 

 

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. kotlin에서 기본적으로 제공하는 다양한 반복문들이 iterator 패턴이라는 것으로 정리 된 것인지 알게 됨

 

B. 어댑터 패턴

 

B. 탬플릿 메서드 

 

 

 


[Reference]

 

JAVA 언어로 배우는 디자인 패턴 입문: 쉽게 배우는 GoF의 23가지 디자인 패턴 | 유키 히로시 | 영진

▶ 3판에서 달라진 점 ㆍ예제 프로그램을 현대 Java 언어(람다식, 확장 for문, enum형 등)로 업데이트 ㆍ이해하기 쉽게 설명하되 현대의 관점에서 예제와 설명(의존성 주입, 보안 관련 설명 등) 수정

ebook-product.kyobobook.co.kr

 

 

💠 반복자(Iterator) 패턴 - 완벽 마스터하기

Iterator Pattern 반복자(Iterator) 패턴은 일련의 데이터 집합에 대하여 순차적인 접근(순회)을 지원하는 패턴이다. 데이터 집합이란 객체들을 그룹으로 묶어 자료의 구조를 취하는 컬렉션을 말한다.

inpa.tistory.com