Octoping의 블로그
article thumbnail

플라이웨이트 패턴의 개념

플라이웨이트 패턴 (Flyweight Pattern)은 공유를 위해 사용되는 패턴으로, 객체를 재사용해서 메모리를 절약하기 위한 목적의 디자인 패턴이다.

 

이 때 객체는 재사용되어야 하므로 공유되는 객체는 불변이어야 한다. 불변 객체가 아니라면 어떤 코드가 플라이웨이트 객체를 임의로 수정했을 때 그 객체를 공유하고 있는 다른 코드에 영향을 미치기 때문이다. (이는 싱글톤과도 비슷하다)

 

시스템에 많은 수의 동일한 불변 객체가 있다면 이 패턴을 사용해서 객체를 플라이웨이트로 설계하고 메모리에 하나의 인스턴스만 보관하여 메모리를 절약할 수 있다.

 

꼭 동일한 객체가 아니더라도 유사도가 높은 객체에 대해 동일한 필드를 추출해서 플라이웨이트로 설계할 수도 있다. 이 때 변하지 않는 불변 속성은 플라이웨이트 객체로 설계하고, 변하는 속성은 이를 사용하는 클라이언트가 정의하도록 한다.

 

플라이웨이트 패턴의 구현

체스 게임에서의 적용

체스 게임을 개발할 때, 각 체스판은 킹, 퀸, 나이트 등의 32개가 존재한다. 그리고 대기실에는 가상의 수천 개의 방이 존재할 수 있다.

이 경우, 각 체스 말에 대해 하나 씩 객체를 할당해주게 된다면, 방이 10000개가 있다 할 때 32만개의 인스턴스가 메모리에 올라가야 한다.

 

이를 전부 메모리에 올리기엔 수가 너무 많고, 메모리를 절약할 수 있는 플라이웨이트 패턴을 사용할 수 있다.

 

// 플라이웨이트를 이용해 변하지 않는 부분만 따로 빼내어 메모리를 절약한다.
class ChessPieceUnit {
    private int id;
    private String name;
    private Color color;

    public static enum Color {
        WHITE, BLACK
    }
}

// 플라이웨이트 객체들을 담고 있는 팩토리 클래스
class ChessPieceUnitFactory {
    private static final Map<Integer, ChessPieceUnit> chessPieces = Map.of(
        1, new ChessPieceUnit(1, "King", BLACK),
        2, new ChessPieceUnit(2, "Queen", BLACK),
        // ...
        16, new ChessPieceUnit(16, "Pawn", WHITE)
    );

    public static ChessPieceUnit getChessPiece(int id) {
        return chessPieces.get(id);
    }
}

체스 말에서 이름, 색은 변하지 않는 속성이므로 플라이웨이트 객체로 만들 수 있다.

그 후, 플라이웨이트 객체들을 캐싱해두고 있는 팩토리 클래스를 만들어주자.

 

class ChessPiece {
    private ChessPieceUnit chessPiece;
    private int positionX;
    private int positionY;
}


class ChessBoard {
    private Map<Integer, ChessPiece> chessPieces = Map.of(
            1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0, 0),
            2, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1, 0)
            // ...
    );

    public void move(int chessPieceId, int toPositionX, int toPositionY) {
        // ...
    }
}

체스 피스 중에 변할 수 있는 Position만 담은 객체를 만든다.

체스 피스들 중에 변하지 않는 속성은 플라이웨이트 객체로 만들고, 변하는 속성 (포지션)은 이를 사용하는 클라이언트인 ChessPiece가 정의하도록 한 것이다.

 

원래라면 id, name, color에 대해서도 각각 메모리를 소비했지만, 이제는 포지션에 대해서만 생성하면 되므로 메모리를 크게 절약할 수 있다.

 

정적 팩토리 메소드와 활용

이펙티브 자바 아이템 1에서는 '생성자 대신 정적 팩토리 메소드를 활용하라'는 챕터가 있는데, 여기에서도 플라이웨이트 패턴이 소개가 된다.

 

생성자에 비해 정적 팩토리 메소드가 갖는 이점 중 하나는, 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다는 점이다.

생성자는 매번 인스턴스를 만들어내야 하지만, 정적 팩토리 메소드는 기존에 캐싱해두었던 인스턴스를 반환할 수 있기 때문에, 플라이웨이트 패턴과 함께 활용해볼 수 있다.

 

Boolean.valueOf

Boolean 클래스는 valueOf라는 정적 팩토리 메소드를 지원한다. 이 함수의 내부 구현은 다음과 같다.

Boolean.valueOf 내부 구현

s가 "true"라는 문자열일 경우 TRUE, 그렇지 않을 경우 FALSE를 반환한다.

 

여기서의 TRUE와 FALSE는 당연히 플라이웨이트 객체다.

매번 new Boolean(true)와 같은 것을 해서 메모리를 낭비하는 것이 아니라, 기존에 캐싱해두었던 플라이웨이트 객체를 반환해서 메모리를 아낀다.

 

이는 Integer.valueOf에서도 비슷하게 찾아볼 수 있다.

Integer.valueOf 내부 구현

 

 

Java의 String에서의 활용

String a = "Octoping";
String b = "Octoping";
String c = new String("Octoping");

System.out.println(a == b);
System.out.println(a == c);

이 코드의 결과는 true, false다.

 

Integer, Boolean의 설계 사상과 비슷하게 String도 플라이웨이트 패턴을 사용해서 동일한 문자열 상수를 재사용한다.

하지만 JVM은 문자열 상수 풀 (String Constant Pool)이라는 문자열 상수를 저장하는 특별한 저장 공간을 사용한다.

 

Boolean과 Integer 클래스는 공유할 객체를 클래스가 적재될 때 한꺼번에 생성하지만 String은 어떤 문자열이 재사용될지 미리 알 수 있는 방법이 없다.

 

그래서 문자열 상수를 처음 사용할 때 문자열 상수 풀에 문자열을 생성하여 저장하고, 그 다음에 사용할 때에는 이미 존재하는 문자열 상수를 꺼내어 사용한다.

 

 

플라이웨이트, 싱글톤, 캐시의 차이

플라이웨이트 패턴의 핵심은 결국 공유, 캐시, 재사용이다.

 

플라이웨이트 vs 싱글톤

싱글톤은 한 클래스가 하나의 객체만 생성할 수 있는 반면, 플라이웨이트는 여러 객체를 생성할 수 있으며, 각각이 여러 코드에서 공유된다.

 

플라이웨이트 패턴은 싱글톤의 변형인 멀티톤 패턴 (다중 인스턴스 패턴)과 유사한 면이 다소 있다. 플라이웨이트와 멀티톤은 코드 구현의 관점에서는 많은 유사점을 가지고 있다. 하지만 플라이웨이트는 객체를 재사용해서 메모리를 절약하는 데에 목적이 있지만, 멀티톤은 객체의 수를 제한하는 데에 목적이 있다.

 

플라이웨이트 vs 캐싱

플라이웨이트  패턴은 팩토리 클래스를 사용해서 생성된 객체를 캐싱한다. 여기서 말하는 캐시는 실제로 저장소를 의미한다.

우리가 일반적으로 언급하는 DB 캐시, CPU 캐시, Memcache 캐시와는 다른데, 이런 캐시는 재사용을 위한 것이라기보다는 주로 액세스 효율성을 개선하기 위한 것이기 때문이다.

 

Redis, HTTP 캐시의 경우

이 두 캐시도 재사용을 목적으로 하고, 플라이웨이트도 재사용을 목적으로 한다.

하지만 이 재사용이라는 두 단어에는 조금 차이가 있다.

 

전자의 재사용은, 매번 요청하지 않고 기존의 것을 가져옴으로써 시간을 절약하는 것이다.

후자는 모든 사용자가 객체를 공유하며, 공간을 절약하는 것이라는 점에서 차이가 있다.

profile

Octoping의 블로그

@Octoping

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!