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);
}
}
}