문제상황
기존 요구사항에서는 다양한 유저 권한이 존재했으며, 이들 각각은 다른 엔티티와의 관계 및 특정 필드를 필요로 했습니다. (예: ADMIN, HUB_MANAGER, HUB_DELIVERY_USER, COMPANY_DELIVERY_USER, COMPANY_MANAGER)
그러나 프로젝트 초기에는 이러한 관계와 필드에 대한 구체적인 요구사항이 명확하지 않아, 추후 권한 추가 및 회원 가입 로직 확장 가능성을 염두에 두어야 했습니다. 실제로 기획이 진행되면서 새롭게 추가된 권한에 맞춰 회원가입 로직을 확장해야 하는 상황도 발생했습니다.
해결방법
이 문제를 해결하기 위해 User 엔티티를 기본으로 하고, 권한별로 하위 엔티티를 두어 User 엔티티를 상속받는 구조로 설계하였습니다. 또한 회원가입 로직은 권한별로 다르게 구현하여, 서비스 로직은 역할(권한)에만 의존하도록 하였습니다. 이렇게 함으로써 권한별로 서로 다른 회원가입 로직을 구현하면서도 캡슐화와 단일 책임 원칙을 지킬 수 있었습니다.
각 권한별로 회원가입 로직을 따로 구현한 후, 이 구현체들을 Spring Bean으로 등록하였으며, 회원 가입 시 유저의 권한에 따라 해당 로직을 처리할 수 있도록 하였습니다.
Adapter - Handler 구조 개발
문제 해결을 위해 고민한 점은, 엔티티에서 단순히 메서드 상속을 통한 추상화 방식으로 해결할지, 아니면 다형성을 활용한 Handler 패턴을 사용할지를 고민했습니다. 결국, 향후 확장성을 고려하여 다형성 기반의 Handler 패턴을 선택하였습니다.
UserSignUpInterface라는 인터페이스를 정의하고, 권한별로 SignUpHandler를 구현하였습니다.
각 Handler는 동일한 인터페이스를 구현하지만, 각기 다른 권한에 맞춰 회원가입 로직을 처리하도록 개발되었습니다.
HandlerAdapter를 통해 유저의 권한에 맞는 Handler를 찾아서 호출하고, 이를 통해 회원가입 로직을 수행하도록 설계했습니다.
SignUpAdapter
@Component
public class SignUpAdapter {
protected static final Map<UserRole, SignUp> signUpHandlerMap = new EnumMap<>(UserRole.class);
@Autowired
public SignUpAdapter(List<SignUp> signUpHandlerList) {
for (SignUp signUpHandler : signUpHandlerList) {
signUpHandlerMap.put(signUpHandler.getPerimitUserRole(), signUpHandler);
}
}
public SignUp getSignUpHandler(UserRole userRole) {
if (!supports(userRole)) {
throw new RoleNotExistsException();
}
return signUpHandlerMap.get(userRole);
}
private boolean supports(UserRole userRole) {
return signUpHandlerMap.containsKey(userRole);
}
}
userService(signUp)
SignUp signUpHandler = signUpAdapter.getSignUpHandler(userRole);
User user = signUpHandler.signUp(username, bCryptPasswordEncoder.encode(password), slackId, hubId, companyId);
서비스에서 SignUpAdapter를 호출하여 유저 권한에 따른 Handler를 찾아 호출하고, 이를 통해 회원가입 로직을 수행합니다. 이 방식으로 서비스는 단순히 Adapter와 역할에만 의존하게 되어 캡슐화와 확장성이 높은 구조를 유지할 수 있습니다.
결과
이 설계를 통해 새로운 권한이나 회원 가입 로직이 추가될 때는, 새로운 Handler만 추가되며 권한에 따른 회원 가입 로직이 수정될 때는 해당 권한의 Handler만 수정하면 되기에 유지보수성과 확장성에서 큰 이점을 얻을 수 있습니다.