JUINTINATION

비지터(Visitor) 패턴 본문

StudyNote

비지터(Visitor) 패턴

DEOKJAE KWON 2024. 6. 26. 02:21
반응형

비지터 패턴이란?

비지터 패턴(방문자 패턴)은 알고리즘을 객체 구조에서 분리시키는 디자인 패턴이다. GOF 디자인 패턴 중 행위 패턴에 해당하며, 이렇게 분리를 하면 구조를 수정하지 않고도 실질적으로 새로운 동작을 기존의 객체 구조에 추가할 수 있게 되는 개방-폐쇄 원칙을 적용하는 방법의 하나이다.

비지터 패턴은 이해하거나 구현하기 매우 어렵고, 심지어 적용하면 코드의 가독성과 유지보수성이 떨어지기 때문에 실제로 거의 사용되지는 않는다. 따라서 매우 특수한 상황이 아니라면 비지터 패턴은 고려할 필요가 없다.

비지터 패턴의 도출 과정

다음 예제를 통해 비지터 패턴이 만들어지는 과정을 살펴보자.

웹 사이트에서 대량의 파일을 크롤링하는데 이 파일들의 형식은 PDF, PPT, Word라고 가정해보자. 이 리소스 파일을 처리하는 도구를 개발해야 하는데 그 기능 중 하나는 리소스 파일에서 텍스트 콘텐츠를 추출하여 텍스트 파일에 저장하는 것이라고 한다면, 이 기능은 어떻게 구현해야 할까?

다음 코드는 이 기능을 구현하는 방법 중 하나이다.

import java.util.ArrayList;
import java.util.List;

abstract class ResourceFile {

    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }

    public abstract void extract2txt();
}

class PPTFile extends ResourceFile {
    public PPTFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // extract text from PPT file
        System.out.println("Extract text from PPT.");
    }
}

class PDFFile extends ResourceFile {
    public PDFFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // extract text from PDF file
        System.out.println("Extract text from PDF.");
    }
}

class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // extract text from Word file
        System.out.println("Extract text from Word.");
    }
}

public class Main {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = new ArrayList<>();

        ResourceFile pptFile = new PPTFile("a.ppt");
        ResourceFile pdfFile = new PDFFile("b.pdf");
        ResourceFile wordFile = new WordFile("c.word");

        resourceFiles.add(pptFile);
        resourceFiles.add(pdfFile);
        resourceFiles.add(wordFile);

        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.extract2txt();
        }
    }
}

여기서 텍스트 콘텐츠 추출 뿐만 아니라 파일의 이름, 크기, 수정, 시간 등 파일 속성도 추출하고, 파일을 압축하거나 인덱스를 빌드하는 등의 새로운 기능이 필요한 상황이 발생하면 개발할 때 다음과 같은 문제가 발생할 수 있다.

  1. 새로운 기능을 추가하기 위해 모든 클래스의 코드를 수정해야 하기 때문에 개방-폐쇄 원칙을 위반한다.
  2. 기능이 추가될수록 그에 따라 각 클래스의 코드도 증가하기 때문에 코드의 가독성과 유지 보수성이 나빠진다.
  3. 모든 상위 계층 비즈니스 로직이 PDFFile, PPTFile, WordFile 클래스에 결합되어 있기 때문에 클래스의 책임이 단일하지 않다.

위의 세 가지 문제를 효과적으로 해결하는 방법은 다음과 같이 비즈니스 코드를 데이터 구조와 분리하여 독립적인 클래스로 설계하는 것이다.

import java.util.ArrayList;
import java.util.List;

abstract class ResourceFile {

    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }
}

class PPTFile extends ResourceFile {
    public PPTFile(String filePath) {
        super(filePath);
    }
}

class PDFFile extends ResourceFile {
    public PDFFile(String filePath) {
        super(filePath);
    }
}

class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }
}

class Extractor {
    public void extract2txt(PPTFile pptFile) {
        // extract text from PPT file
        System.out.println("Extract text from PPT.");
    }

    public void extract2txt(PDFFile pdfFile) {
        // extract text from PDF file
        System.out.println("Extract text from PDF.");
    }

    public void extract2txt(WordFile wordFile) {
        // extract text from Word file
        System.out.println("Extract text from Word.");
    }
}

public class Main {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        Extractor extractor = new Extractor();

        ResourceFile pptFile = new PPTFile("a.ppt");
        ResourceFile pdfFile = new PDFFile("b.pdf");
        ResourceFile wordFile = new WordFile("c.word");

        resourceFiles.add(pptFile);
        resourceFiles.add(pdfFile);
        resourceFiles.add(wordFile);

        for (ResourceFile resourceFile : resourceFiles) {
            extractor.extract2txt(resourceFile); // 컴파일 오류 발생
        }
    }
}

이 코드의 핵심은 텍스트 내용을 추출하는 작업을 오버로딩 기반으로 설계했다는 것이다.

하지만 이 코드는 컴파일되지 않는데, 그 이유는 다음과 같다.

  • 다형성은 실행 시간에 객체의 실제 유형을 가져와 실제 유형에 해당하는 메서드를 실행하는 동적 바인딩인데 반해, 함수 오버로딩은 정적 바인딩의 일종으로 컴파일 시에는 객체의 실제 유형을 알 수 없지만 선언된 유형에 해당하는 메서드가 실행된다.
  • 위의 코드에서 resourceFiles에 포함된 객체는 ResourceFile 유형으로 선언되어 있지만, 정작 Extractor 클래스에는 ResourceFile 유형이 매개변수인 extract2txt() 함수가 정의되어 있지 않기 때문에 컴파일 단계에서 실패하게 된다.

이 문제에 대한 해결 방법은 다음과 같다.

import java.util.ArrayList;
import java.util.List;

abstract class ResourceFile {

    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }

    public abstract void accept(Extractor extractor);
}

class PPTFile extends ResourceFile {
    public PPTFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }
}

class PDFFile extends ResourceFile {
    public PDFFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }
}

class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }
}

class Extractor {
    public void extract2txt(PPTFile pptFile) {
        // extract text from PPT file
        System.out.println("Extract text from PPT.");
    }

    public void extract2txt(PDFFile pdfFile) {
        // extract text from PDF file
        System.out.println("Extract text from PDF.");
    }

    public void extract2txt(WordFile wordFile) {
        // extract text from Word file
        System.out.println("Extract text from Word.");
    }
}

public class Main {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        Extractor extractor = new Extractor();

        ResourceFile pptFile = new PPTFile("a.ppt");
        ResourceFile pdfFile = new PDFFile("b.pdf");
        ResourceFile wordFile = new WordFile("c.word");

        resourceFiles.add(pptFile);
        resourceFiles.add(pdfFile);
        resourceFiles.add(wordFile);

        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(extractor);
        }
    }
}

main 메서드에서 다형성을 기반으로 실제 유형인 PPTFile, PDFFile, WorldFile 클래스의 accept aㅔ서드를 호출한다. 만약 WordFile 클래스의 accept() 메서드가 호출되었다고 가정하면, WordFile 클래스의 accept() 메서드의 매개변수 this는 WordFile 클래스 객체가 되며, 이는 컴파일 시 미리 결정된다.

따라서 WordFile 클래스의 accept() 메서드는 Extractor 클래스의 extract2txt(WordFile wordFile) 오버로드 메서드를 호출한다.

또한 이 코드 자체가 이미 비지터 패턴의 원형이라고도 볼 수 있다. 여기에 리소스 파일의 형식에 따라 각자 최적화된 압축 알고리즘을 사용하여 압축하는 새로운 기능을 추가해야 한다면 Extractor 클래스와 유사한 Compressor 클래스를 구현하고, 이번에도 리소스 파일의 형식에 따라 리소스 파일을 압축하는 여러 개의 오버로드 메서드와 각 리소스 파일 클래스에서 새로운 accept() 오버로드 메서드를 정의해야 한다.

import java.util.ArrayList;
import java.util.List;

abstract class ResourceFile {

    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }

    public abstract void accept(Extractor extractor);

    public abstract void accept(Compressor compressor);
}

class PPTFile extends ResourceFile {
    public PPTFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }

    @Override
    public void accept(Compressor compressor) {
        compressor.compress(this);
    }
}

class PDFFile extends ResourceFile {
    public PDFFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }

    @Override
    public void accept(Compressor compressor) {
        compressor.compress(this);
    }
}

class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Extractor extractor) {
        extractor.extract2txt(this);
    }

    @Override
    public void accept(Compressor compressor) {
        compressor.compress(this);
    }
}

class Extractor {
    public void extract2txt(PPTFile pptFile) {
        // extract text from PPT file
        System.out.println("Extract text from PPT.");
    }

    public void extract2txt(PDFFile pdfFile) {
        // extract text from PDF file
        System.out.println("Extract text from PDF.");
    }

    public void extract2txt(WordFile wordFile) {
        // extract text from Word file
        System.out.println("Extract text from Word.");
    }
}

class Compressor {
    public void compress(PPTFile pptFile) {
        // compress PPT file
        System.out.println("Compress PPT");
    }

    public void compress(PDFFile pdfFile) {
        // compress PDF file
        System.out.println("Compress PDF");
    }

    public void compress(WordFile wordFile) {
        // compress Word file
        System.out.println("Compress Word");
    }
}

public class Main {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        // Extractor extractor = new Extractor();
        Compressor compressor = new Compressor();

        ResourceFile pptFile = new PPTFile("a.ppt");
        ResourceFile pdfFile = new PDFFile("b.pdf");
        ResourceFile wordFile = new WordFile("c.word");

        resourceFiles.add(pptFile);
        resourceFiles.add(pdfFile);
        resourceFiles.add(wordFile);

        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(compressor);
        }
    }
}

결론

학기 중에 이런저런 핑계를 대면서 모든 활동을 올 스톱했다가 종강하자마자 뭐라도 해야겠다 싶어서 정처기 공부할 때 봤다가 왠지 모르게 흥미가 생겼었던 비지터 패턴에 대해 알아보았다.
처음 SOLID에 대해 공부할 때부터 개방-폐쇄 원칙을 어떻게 지켜야 한다는 말일까 고민을 많이 해서 더욱 관심이 생겼었는데, 코드의 가독성과 유지 보수성이 떨어지기 때문에 실제로 거의 사용되지는 않는다는 구절을 보자마자 처음 공부할 때 이해가 안 됐던 부분의 이유가 있었구나 하는 생각이 들었다.
책에 비지터 패턴에 대해 설명할 때 이중 디스패치가 빠질 수 없다는 말도 있었는데 지금 굳이 적을 필요는 없을 것 같다는 생각이 들었고, 나중에 더 깊게 공부할 기회가 생기게 된다면 글을 작성해 볼 생각이다.

728x90
Comments