java, 반복되는 로직의 흐름을 강제하기 위한 탬플릿 패턴 - Comparable과 함수형 인터페이스

특정 로직 흐름을 명시하고 강제할 필요가 있다. 이를 if문으로 해소할 경우 복잡해진다.

  • 회원의 등급이 두 개가 있다. 슈퍼 어드민 - 일반 어드민
  • 일반 어드민은 자신이 작성한 글을 수정할 수 있지만 남이 작성한 글을 수정할 수 없다. 슈퍼 어드민은 누가 쓴 글이든 수정할 수 있다.
  • 이 경우 코드로 작성하면 어떻게 될까?
public class AdminServiceWithIfTest {
    @Test
    void test(){
        // 회원
        Admin supers = new Admin("super", Admin.Type.SUPER);
        Admin lee = new Admin("lee", Admin.Type.BASIC);
        Admin kim = new Admin("kim", Admin.Type.BASIC);

        // 기존에 작성된 글
        Article writtenArticle = new Article(324l, "공지사항", "내일 잠시 점검이 있을 예정입니다.", "kim");

        // 수정의 권한이 있는지 확인하고 한다.
        Admin modifier = supers;

        // if 문으로 데이터에 대한 접근 권한을 판별한다.
        // 깨지기 쉬운 코드가 된다.
        if(modifier.type == Admin.Type.SUPER){
            System.out.println("super 유저는 수정 가능합니다...");
        }else if(modifier.id.equals(writtenArticle.author)){
            System.out.println("작성자와 수정자가 일치하여 수정 가능합니다...");
        }else{
            throw new IllegalArgumentException("수정 권한이 없습니다.");
        }

        // 수정한다.
        writtenArticle.changeTitleAndContent("공지사항(수정)", "점검이 조기에 종료되어 오늘 자정 전에 오픈 예정입니다.");
    }
}
  • 특정 데이터를 수정하기 위한 로직을 보면 if문이 사용됨을 확인할 수 있다.
    • super admin의 여부
    • id 일치의 여부
    • 그 외
  • 해당 로직은 반복적으로 사용될 것으로 보인다. 글의 수정부터 시작하여 회원, 주문 등은 슈퍼유저의 여부와 해당 데이터의 소유자 여부를 계속적으로 판별해야하기 때문이다.
  • 그러한 로직 마다 반복하여 if문 작성하는 것은 복잡하고 깨지기 쉽다. 차라리 if문의 로직이 분명하다면 이를 일종의 템플릿으로 구현하고 해당 로직을 강제하는 것이 나을 것이다.
  • Comparable의 디자인 패턴을 따라하고 이를 동작시킬 탬플릿을 구현하여 이 문제를 해소하였다.

객체의 비교를 위한 인터페이스 구현

  • 가장 먼저 각 객체의 비교를 위한 인터페이스를 구현하였다. 자바의 비교를 위한 주요 인터페이스 중 하나인 Comparable의 형태를 따랐다.
public interface Auth<U> {
    boolean isSuper();
    boolean isTheSameIdWith(U id);
}

public class Admin implements Auth<Long> {
    private final Long id;
    private final Type type;

    public enum Type{
        SUPER, BASIC
    }

    public Admin(Long id, Type type) {
        this.id = id;
        this.type = type;
    }

    @Override
    public boolean isTheSameIdWith(Long id) {
        return this.id.equals(id);
    }

    @Override
    public boolean isSuper() {
        return type == Type.SUPER;
    }
}
  • 위의 코드를 보면 isSuper와 isTheSameIdWith를 구현한다. 각 각 ‘super admin의 여부’와 ‘id 일치의 여부’이며 둘 다 false이면 ‘그 외’ 조건이 된다.
  • 이제 이 로직을 강제할 로직을 구현하자.

로직 강제를 위한 탬플릿과 각 상황에 따른 로직을 함수형 인터페이스로 구현

  • 탬플릿의 코드에 앞서 실제 사용을 먼저 작성하였다. 그 내용은 아래와 같다.
@Test
    void log() {
        // given
        final AdminAuthBiConsumer<Admin, Long> logAccessUser = new AdminAuthBiConsumer.Builder<Admin, Long>()
                .supers((a, id) -> System.out.println("슈퍼유저"))
                .theSameId((a, id) -> System.out.println("자기 자신의 데이터에 접근"))
                .elses((a, id) -> {throw new IllegalArgumentException("허용되지 않은 접근");})
                .build();

        // 슈퍼유저
        logAccessUser.compareTo(new Admin(999L, SUPER), 1L); 

        // 자기 자신의 데이터에 접근
        logAccessUser.compareTo(new Admin(1L, BASIC), 1L);

        // 다른 회원의 데이터 접근
        assertThatThrownBy(()->{
            logAccessUser.compareTo(new Admin(1L, BASIC), 2L);
        }).isInstanceOf(IllegalArgumentException.class);
    }
  • AdminAuthBiConsumer 객체를 빌더패턴으로 초기화 한다.
  • 필더패턴의 필더는 각각 상황에 따른 로직으로서 supers, theSameId, elses 이다. 함수형 인터페이스로 작성한다.
  • 초기화된 객체는 compareTo(admin, id) 메서드로 사용한다. 첫 번째 인자는 요청자, 두 번째 객체는 요청한 값이다.
  • 이처럼 AdminAuthBiConsumer를 탬플릿으로 구현하여, 특정 로직을 강제하고 재사용할 수 있도록 구현하였다.

AdminAuthBiConsumer 코드

  • 해당 코드는 아래와 같이 작성하였다.
  • 불변 객체로 구현하였으며 빌드 패턴을 사용하였다.
  • 더 많은 내용과 실제 유닛 테스트를 한 내용은 다음 깃 소스로 확인할 수 있다 : https://github.com/infoqoch/openstudy/tree/master/java-auth-template
import java.util.function.BiConsumer;

public class AdminAuthBiConsumer<E extends Auth, U> {
    private final BiConsumer<E, U> supers;
    private final BiConsumer<E, U> theSameId;
    private final BiConsumer<E, U> elses;

    public AdminAuthBiConsumer(Builder<E, U> builder) {
        supers = builder.supers;
        theSameId = builder.theSameId;
        elses = builder.elses;
    }

    public void compareTo(E target, U u) {
        if(target.isSuper()) {
            supers.accept(target, u);
        }else if(target.isTheSameIdWith(u)) {
            theSameId.accept(target, u);
        }else {
            elses.accept(target, u);
        }
    }

    public static class Builder<E extends Auth, U> {
        private BiConsumer<E, U> supers;
        private BiConsumer<E, U> theSameId;
        private BiConsumer<E, U> elses;

        public Builder() {
        }

        public Builder<E, U> supers(BiConsumer<E, U> consumer){
            this.supers = consumer;
            return this;
        }

        public Builder<E, U> theSameId(BiConsumer<E, U> consumer){
            this.theSameId = consumer;
            return this;
        }

        public Builder<E, U> elses(BiConsumer<E, U> consumer){
            this.elses = consumer;
            return this;
        }

        public AdminAuthBiConsumer<E, U> build() {
            return new AdminAuthBiConsumer<E, U>(this);
        }
    }
}