본문 바로가기

디자인 패턴

[디자인패턴 / JAVA] 프렌드 접근자 패턴

[디자인패턴 / JAVA] 프렌드 접근자 패턴

API 개발하다보면 모듈화와 접근제어자의 노출도를 두고 트레이드 오프하는경우가 있다.
예를들어, API패키지를 지원하는 NONE-API패키지가 있을때, API패키지에서 NONE-API패키지의 메소드에 접근하기 위해선 메소드를 "public"으로 설정해야한다.

package test.api;

public final class api{
    private final NoneApi noneApi;
    
    {
        noneApi = new noneApi();
    }
    
    public void hello(){
        noneApi.hello();
    }

}
package test.noneapi;

public final class NoneApi{
	
    public void hello(){
    	return "hello";
    }
    
}

하지만 이 와 같은 설계는 잠재적인 클라이언트가 NoneApi(제작자가 API의 일부로 생각하지 않았던)클래스의 메소드를 호출해 우리의 본 목적과는 다르게 사용 할 수 있다는 위험성이 있다.

이 문제는 모듈화를 깨면 해결이 가능은 한데 좋은 방법처럼 보이지는 않는다.

package test.api;

public final class Api{
    private final NoneApi noneApi;
    
    public void hello(){
        noneApi.hello();
    }

}

package test.api;

final class NoneApi{
	
    NoneApi(){}
    
    void hello(){
    	return "hello";
    }
    
}

실제로 다른 행위를 하는 a.impl과 a모듈을 a로 합쳐 NoneApi클래스의 오용을 막는데 성공했다.

이 처럼 모듈화를 고도화 시킬수록 메소드의 노출도는 증가할 수 있는데, 여기 두마리 토끼를 잡는 방법이 있다.

프렌드 접근자 패턴

프렌드 접근자 패턴을 사용하면, 모듈화는 유지하면서 메소드의 오용을 완벽히 막을 수 있는데,
API모듈에 Accessor를 두고 NoneApi모듈에 AccessorImpl(Accessor를 구현하는)을 둔다.
AccessorImpl은 NoneApi메소드를 프록시처럼 호출하며 클라이언트의 오용을 막을수 있고 NoneApi를 생성하며 그렇게 할 수 있다.

핵심은 protected를 이용한 모듈간의 데이터 전달이다.

코드를 보자.

우선 test.api모듈의 코드다.
Accessor의 경우, 다른 모듈에서 구현해야하기때문에, public class로 만들고, 사용은 자신이 속한 모듈만 사용해야하기 때문에, 메소드 접근제어는 전부 protected로 만든다.

package test.api;

public abstract class Accessor{

    private static Accessor accessor;
    
    protected static registerAccessor(Accessor accessor){
        if(Accessor.accessor != null) throw new IllegalStateException();
        Accessor.accessor = accessor;
    }
    
    static getAccessor(){
        return Accessor.accessor;
    }
    
    protected NoneApi getNoneApi();
    
}


이제, test.noneapi 모듈의 구현을 보자.

package test.noneapi;

final class NoneApi{
	
    NoneApi(){}
    
    public void hello(){
    	return "hello";
    }
    
}

위 코드를 보면, 메소드의 접근제어자가 public으로 설정되어있는걸 볼 수 있는데, 이 부분은 사실 크게 상관없다. 어차피 class레벨이 package-private이고, 생성자 또한 package-private이므로, (우리의 API를 사용할)클라이언트는 일반적인 방법으로는 NoneApi객체를 생성할수조차 없다. 이는 test.api모듈도 마찬가지인데, 이를 가능하게 해주는것이 Accessor이다.

final class AccessorImpl extends Accessor{

    AccessorImpl(){}

    @Override
    protected NoneApi getNoneApi(){
        return new NoneApi();
    }

}

위 코드를 보면, AccessorImpl의 클래스 레벨이 package-private이며, 생성자또한 package-private이다. 우리의 본래 목적인 NONE-API의 오용을 막기위해 클래스 레벨을 package-private으로 만들어야한다.
그렇지 않으면 클라이언트는 AccessorImpl을 생성하고, NoneApi에 접근할것이다.
이제, AccessorImpl을 Accessor에 등록 시켜주기만 하면 되는데, 이는 정적 초기화 블록을 사용하면 가능하다. AccessorImpl과 같은 모듈에서 정적 초기화 블록이 동작할 수 있는위치에 다음 코드를 삽입한다.

static{
    Accessor.registerAccessor(new AccessorImpl());
}


이제, API패키지에서는 다음과 같이 NoneApi 클래스의 메소드를 사용할 수 있다.

package test.api;

public final class Api{
    private final NoneApi noneApi;
    
    {
        noneApi = Accessor.getAccessor().getNoneApi();
    }
    
    public void hello(){
        noneApi.hello();
    }

}

이 패턴은 "자바 API 디자인"이라는 책에서 처음 알게되었고,
실제로 동작하는 코드는 위 책의 공식 레포지토리인 에서 확인할 수 있다.
http://wiki.apidesign.org/wiki/Sources

Sources - APIDesign

From APIDesign All the code snippets shown in the Practical API Design book are extracted from real projects. As such it is guaranteed they are correct, they compile and can even be executed, debugged and tested. Moreover, it is often not enough to see jus

wiki.apidesign.org