Iterator 패턴이란? (예제편)

갈아만든배·2023년 5월 8일
0

디자인 패턴 정리

목록 보기
2/3

해당 내용은 "JAVA 언어로 배우는 디자인 패턴 입문 3판"을 읽으면서 내용을 정리한 것입니다.

이제 예제를 통해 내용을 정리하며 이해하는 시간을 가져보자.

설명편에서 예시를 들었던 해리포터라는 소설의 시리즈를 관점으로 예제를 생성해보자.

먼저 해리포터라는 소설 시리즈는 총 7부작의 소설의 모음인데, 이 관점에서 볼 때 해리포터 시리즈는 해리포터라는 객체를 7개 가진 Iterable 객체라고 볼 수 있다.

이를 JAVA 문법으로 표현하면,

	Iterable<해리포터> 해리포터 시리즈

로 볼 수 있다.

이제 이를 토대로 Kotlin을 이용해 예제를 작성해보자.

우선 해리포터라는 책을 객체로 만들어보자.

data class Harry(val title: String)

간단한 예제로 작성하기 위해 해리포터 객체는 title 변수를 가진 데이터 클래스로 작성했다.

이제 해리포터 시리즈 객체를 생성해보자.

class HarrySeries : Iterable<Harry> {
    private var series = arrayOf(
        Harry("해리포터와 마법사의 돌"),
        Harry("해리포터와 비밀의 방"),
        Harry("해리포터와 아즈카반의 죄수"),
        Harry("해리포터와 불의 잔"),
        Harry("해리포터와 불사조 기사단"),
        Harry("해리포터와 혼혈 왕자"),
        Harry("해리포터와 죽음의 성물")
    )

    override fun iterator(): Iterator<Harry> {
        TODO("Not yet implemented")
    }
}

편의를 위해 내부에 series라는 Harry 객체 배열을 가진 Iterable 인터페이스를 구현한 HarrySeries라는 Iterable 객체를 생성했다.

우리는 Iterable 인터페이스를 상속받은 구현체로 HarrySeries 객체를 선언했기 때문에 Iterator 객체를 반환할 iterator() 메소드를 구현해야 한다.

이제 Iterator 인터페이스를 구현할 HarryIterator 객체를 만들어보자.

Iterator는 앞서 설명한 대로 반복 가능한 반복체를 반복하여 호출하며 반복체 내부의 값을 반환하는데,

여기서 우리는 Iterable 인터페이스 구현체인 HarrySeries 객체에 대한 Iterator를 반환할 객체를 구현하는 것이므로 HarryIterator 객체의 생성자에 내부 배열인 series를 전달하여 series를 반복하게끔 구현할 것이다.

이제 HarryIterator 객체를 생성해보자.

class HarryIterator(series: Array<Harry>) : Iterator<Harry> {

    override fun hasNext(): Boolean {
        TODO("Not yet implemented")
    }

    override fun next(): Harry {
        TODO("Not yet implemented")
    }
}

객체의 생성자에 series를 전달했다.

이제 내부 구현에 해당 객체를 사용할 것이다.

그리고 HarryIterator를 선언했으므로, 아까 미구현으로 남겨두었던 iterator() 메소드를 구현해보자.

override fun iterator(): Iterator<Harry> {
	return HarryIterator(series)
}

iterator() 메소드 호출 시 HarryIterator를 생성해 반환하는 것으로 구현했다. 이후 우리가 전달받은 배열을 이용해 HarryIterator 객체를 잘 구현하기만 하면, 이를 사용하는 부분에서는 HarryIterator의 내부 구현을 이해하지 않아도 Iterator 인터페이스를 사용하던 그대로 사용할 수 있게 된다.

이 때, Iterable 객체와 Iterator 객체는 의존성을 가지게 된다.

Iterable 객체의 내부 반복체의 형태를 해당 반복체 속의 값들을 반환해야 하는 Iterator 객체는 반드시 알고 있어야 해당 반복체를 이용해 값을 반환할 수 있기 때문이다.

하지만 두 객체 사이의 반복체에 관한 의존성만 지켜준다면, 이를 사용하는 곳에서는 Iterable 객체와 Iterator 객체의 내부 구현에 대해 자세히 알고 있지 않고도, Iterable 인터페이스와 Iterator 인터페이스를 이용하던 그대로 사용할 수 있게 된다.

이제 HarryIterator 객체의 내부 구현을 확인해보자.

class HarryIterator(series: Array<Harry>) : Iterator<Harry> {
    private var series = series
    private var index = 0


    override fun hasNext(): Boolean {
        return index < series.size
    }

    override fun next(): Harry {
        return series[index++]
    }
}

초기에 생성했던 것과 달리, 내부 배열이 현재 가리키고 있던 위치를 저장할 index 변수가 생겼다. 해당 변수는 next() 메소드가 호출될 경우, 하나씩 증가할 것이다.

이제 Iterator 객체를 통해 Harry 객체를 반환받을 수 있을 것이다.

while (harryIterator.hasNext()) {
	val harry = harryIterator.next()

	println(harry.title)
}

hasNext() 메소드와 next() 메소드의 사용법만 알면 내부 구현을 신경쓰지 않고도 이처럼 원하는 값을 얻을 수 있다.

이제 HarrySeries의 내부 배열이 List로 변경되든, 별도의 배열 구현체를 이용해 구현하든 간에 상관 없이 해당 구현체 대로 hasNext() 메소드와 next() 메소드가 동작하게끔 수정만 해주면,

기존에 작성해 두었던 해당 Iterator 객체를 사용하던 기존 객체는 내부 배열에 대한 의존성을 가지지 않게 구현할 수 있게 된다.

만일 Iterable 객체와 Iterator 객체를 이용하지 않고 단순 배열을 이용해 해당 반복을 처리했을 경우, 반복자에 대한 구현이 변경될 때 마다 해당 반복자를 이용하던 모든 소스를 다 수정하고 다시 테스트를 진행했어야 하지만,

Iterable 객체와 해당 객체의 Iterator 객체를 이용해 구현했을 경우에는 Iterable 객체의 내부 반복자가 변경됐을 경우에도, 해당 반복자에 대한 Iterator 객체의 구현만 변경해주면 수정한 Iterator가 정상 동작 한다는 가정 하에 다른 소스를 테스트하지 않아도 될 것이다.

이에 대한 보증은 반복자에 대한 재사용성을 증가시킬 수 있는 아주 좋은 디자인 패턴 중 하나이다.

0개의 댓글