인생의 순회자가 되는 법
음악을 듣는 방법을 생각해 보죠. 우리 주변에는 다양한 음악 재생 장치들이 있습니다. CD 플레이어는 물리적인 디스크에 트랙 순서대로 음악을 저장합니다. 스트리밍 서비스는 클라우드 서버의 데이터베이스에서 플레이리스트 형태로 음악을 관리합니다. USB 메모리는 파일 시스템 구조로, 바이닐 레코드는 아날로그 방식으로 음악을 담고 있습니다.
각각의 저장 방식은 완전히 다릅니다. CD는 물리적 트랙, 스트리밍은 네트워크 연결 리스트, USB는 디렉터리 구조, 바이닐은 물리적 홈의 연속이죠. 하지만 우리가 음악을 들을 때 사용하는 리모컨의 기능은 일관됩니다. 어떤 장치든 "다음 곡", "이전 곡", "현재 재생 중인 곡" 버튼이 있고, 동일한 방식으로 작동합니다.
복잡하고 다양한 음악 저장 방식(컬렉션)을 가진 장치들에서, 사용자에게는 단순하고 일관된 조작 방법을 제공합니다. 리모컨이 완벽한 이터레이터(반복자) 역할을 하며, 사용자는 내부 구조를 모르더라도 원하는 음악에 순차적으로 접근할 수 있습니다.
건축가 Christopher Alexander은 도시와 건물을 설계할 때 반복되는 문제들과 그 해결책들을 '패턴'이라고 명명했습니다. 마치 "창문은 얼마나 높이 있어야 하는가", "건물은 몇 층이어야 하는가"와 같은 질문들에 대한 검증된 답안 같은 것이었습니다. 이를 1994년, GOF는 Alexander의 아이디어를 소프트웨어 세계로 가져와 ‘이터레이터 패턴’이라고 공식적으로 명명했습니다.
이터레이터 패턴은 "컬렉션의 내부 구조를 노출하지 않고 순차적으로 요소에 접근하는 방법을 제공한다"는 목표를 가지고 있습니다. 간단해 보이지만, 이 패턴은 소프트웨어 설계에서 만나는 가장 근본적인 문제 중 하나를 해결합니다.
친숙한 음악 플레이어를 예시로 들겠습니다.
// 음악 클래스
class Song {
private String title;
private String artist;
public Song(String title, String artist) {
this.title = title;
this.artist = artist;
}
@Override
public String toString() {
return title + " - " + artist;
}
}
// 만능 리모컨 인터페이스 (Iterator)
interface MusicIterator {
boolean hasNext(); // 다음 곡이 있나요?
Song next(); // 다음 곡으로 넘어가세요
Song current(); // 현재 곡이 뭔가요?
}
// 음악 저장소 인터페이스 (Iterable)
interface MusicCollection {
MusicIterator createIterator();
String getCollectionType();
}
// CD 플레이어 (배열 방식)
class CDPlayer implements MusicCollection {
private Song[] tracks;
private String albumName;
public CDPlayer(String albumName, int maxTracks) {
this.albumName = albumName;
this.tracks = new Song[maxTracks];
}
public void insertTrack(int position, Song song) {
if (position >= 0 && position < tracks.length) {
tracks[position] = song;
}
}
@Override
public String getCollectionType() {
return "CD: " + albumName;
}
@Override
public MusicIterator createIterator() {
return new CDIterator();
}
// CD 전용 리모컨
private class CDIterator implements MusicIterator {
private int currentPosition = 0;
@Override
public boolean hasNext() {
for (int i = currentPosition + 1; i < tracks.length; i++) {
if (tracks[i] != null) return true;
}
return false;
}
@Override
public Song next() {
for (int i = currentPosition + 1; i < tracks.length; i++) {
if (tracks[i] != null) {
currentPosition = i;
return tracks[i];
}
}
throw new RuntimeException("더 이상 재생할 곡이 없습니다!");
}
@Override
public Song current() {
return (currentPosition >= 0 && currentPosition < tracks.length)
? tracks[currentPosition] : null;
}
}
}
// 스트리밍 플레이리스트 (연결 리스트 방식)
class StreamingPlaylist implements MusicCollection {
private PlaylistNode head;
private String playlistName;
private static class PlaylistNode {
Song song;
PlaylistNode next;
PlaylistNode(Song song) {
this.song = song;
}
}
public StreamingPlaylist(String playlistName) {
this.playlistName = playlistName;
this.head = null;
}
public void addSong(Song song) {
PlaylistNode newNode = new PlaylistNode(song);
if (head == null) {
head = newNode;
} else {
PlaylistNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
@Override
public String getCollectionType() {
return "스트리밍 플레이리스트: " + playlistName;
}
@Override
public MusicIterator createIterator() {
return new StreamingIterator();
}
// 스트리밍 전용 리모컨
private class StreamingIterator implements MusicIterator {
private PlaylistNode current = head;
@Override
public boolean hasNext() {
return current != null && current.next != null;
}
@Override
public Song next() {
if (hasNext()) {
current = current.next;
return current.song;
}
throw new RuntimeException("더 이상 재생할 곡이 없습니다!");
}
@Override
public Song current() {
return current != null ? current.song : null;
}
}
}
// 통합 음악 플레이어
public class UniversalMusicPlayer {
public static void main(String[] args) {
// CD 플레이어 설정
CDPlayer myCD = new CDPlayer("추억의 발라드", 3);
myCD.insertTrack(0, new Song("첫사랑", "이문세"));
myCD.insertTrack(1, new Song("그대에게", "무한궤도"));
myCD.insertTrack(2, new Song("사랑할수록", "심수봉"));
// 스트리밍 플레이리스트 설정
StreamingPlaylist myPlaylist = new StreamingPlaylist("운동할 때 듣는 신나는 곡");
myPlaylist.addSong(new Song("Dynamite", "BTS"));
myPlaylist.addSong(new Song("Next Level", "aespa"));
myPlaylist.addSong(new Song("Permission to Dance", "BTS"));
// 동일한 방식으로 제어
playMusicCollection(myCD);
playMusicCollection(myPlaylist);
}
// 어떤 종류의 음악 소스든 동일한 방식으로 재생
public static void playMusicCollection(MusicCollection collection) {
System.out.println("\n� " + collection.getCollectionType() + " 재생 시작");
MusicIterator remote = collection.createIterator();
Song current = remote.current();
if (current != null) {
System.out.println("� 현재 재생중: " + current);
}
while (remote.hasNext()) {
Song next = remote.next();
System.out.println("⏭️ 다음 곡: " + next); } System.out.println("⏹️ 재생 종료"); } }
위 코드에서 playMusicCollection 함수는 CD든 스트리밍 플레이리스트든 상관없이 동일한 방식으로 음악을 재생합니다.
내부적으로 CD는 배열로, 스트리밍 플레이리스트는 연결 리스트로 구현되어 있습니다. 이 둘의 데이터 구조는 완전히 다르지만, 클라이언트 코드(playMusicCollection)는 이런 차이를 전혀 알 필요가 없습니다. 단지 MusicIterator라는 공통 인터페이스만 사용하면 됩니다.
이처럼 이터레이터 패턴의 핵심은 복잡한 내부 구조를 숨기는 ‘캡슐화’, 다양한 컬렉션을 동일한 방식으로 다루는 ‘일관성’, 새로운 음악 소스(라디오, 바이닐 등)가 추가되어도 기존 코드는 변경하지 않는 ‘확장성’입니다.
마치 우리가 서로 다른 리모컨을 가진 TV, 에어컨, 오디오를 하나의 만능 리모컨으로 조작할 수 있는 것과 같습니다.
개개인은 가족, 친구, 직장, 취미, 건강 등 수많은 영역들이 서로 다른 방식으로 구성되어 있습니다. 하지만 성공적인 사람들은 이 모든 복잡함을 관통하는 몇 가지 일관된 원칙(이터레이터)을 가지고 있습니다. 또한 상대방이 어떤 성격이든, 어떤 배경을 가지든, 동일한 방식으로 그들과 소통할 수 있어야 하며, 새로운 기술이 나오고, 새로운 환경이 조성되고, 새로운 사람들을 만나게 되어도, 핵심 가치와 원칙들은 그대로 적용될 수 있어야 합니다.
인생의 복잡한 문제들을 단순하고 일관된 방식으로 해결할 수 있는 원칙들을 세우고, 그것을 통해 어떤 상황에서든 차분하게 순차적으로 앞으로 나아가야 합니다.