본문 바로가기

Spring

Enum 을 BeanProvider로 사용해보기

대략 이런 구조의 여러개의 도메인 루트들이

여러 셋팅 값들의 집합인 Mapper 라는 엔티티를 도메인 루트로 지니는 도메인과

의존성을 맺고 있다고 예시를 들겠습니다.

 

이때, 각 루트 엔티티의 ID 로 Mapper 의 ID 를 조회하는 API 가 필요하다는 요구사항이 있었습니다.

 

단순히 생각하면, 3개의 API 를 생성하는것으로 해결할 수 있지만, 다른 방법을 찾고 싶었고

아래와 같이 어떤 도메인인지 Enum 을 받아 처리하는 API 를 구현하기로 했습니다.

GET /settings/owners/{owner}/{id}

ex) /settings/owners/PROJECT/50a10f9e-32b5-4a0d-8773-fa2413cd0a75

 

 

처음엔 단순히 구현해봅니다.

@RequiredArgsConstructor
@Service
public class Service {
	
    private final CompanyRepository companyRepository;
    private final ProjectRepository projectRepository;
    private final StaffRepository staffRepository;
    
    public Long findId(Owner owner, String id) {
    
    	switch (owner) {
        	case COMPANY:
            	companyRepository.findMapperIdById(id);
            case PROJECT:
            	projectRepository.findMapperIdById(id);
            case STAFF:
            	staffRepository.findMapperIdById(id);
        }
    }
}

위와 같은 코드를 보니 매우 심기가 불편해졌습니다.

 

1.  요구사항의 중요도에 비해 Service 가 가지게 되는 Repository 와의 의존성이 너무 많아집니다.

 

2. 의도를 파악하기 힘들고, 코드 변경의 이유가 발생하며, 한 메서드 내에서 너무 많은 케이스가 발생하는 switch 문은 최대한 낮은 레벨의 클래스에 구현을 숨기고 싶습니다.

 

Repositoryt Provider 로써의 책임을 할당 받을 클래스가 필요하다고 느끼고

그 대상을 찾던 중 Owner Enum 이 눈에 들어왔습니다.

 

1. 오직 이 요구사항을 위해서 새로 생성된 클래스 이기 때문에 클래스가 지닌 책임 자체가 순수하여 섞일일이 없음.

 

2. Bean 에 대한 의존성을 지녀야 하기 때문에 어플리케이션 실행 시점에서 메모리가 할당되어 있었으면 함, 굳이 별도의 빈을 생성하고 싶지 않았음.

 

등의 이유가 있었고 즉시 코드를 구현해보았습니다.

 

완성된 소스는 다음과 같습니다.

 

public interface SettingOwnerRepository {
  String findMapperId();
}

public interface CompanyRepository implements SettingOwnerRepository {}
public interface ProjectRepository implements SettingOwnerRepository {}
public interface StaffRepository implements SettingOwnerRepository {}

public enum SettingOwner {
    COMPANY,
    PROJECT,
    STAFF

    @Setter
    private SettingOwnerRepository settingOwnerRepository;

    public String findMapperId(String ownerId) {
        return settingOwnerRepository.findMapperId(ownerId);
    }

    @Component
    private static class OwnerRepositoryInjector {
        private CompanyRepository companyRepository;
        private ProjectRepository projectRepository;
        private StaffRepository staffRepository;

        @PostConstruct
        public void init() {
            for (SettingOwner settingOwner : EnumSet.allOf(SettingOwner.class)) {
                if (settingOwner == COMPANY) {
                    settingOwner.setSettingOwnerRepository(companyRepository);
                } else if (settingOwner == PROJECT) {
                    settingOwner.setSettingOwnerRepository(projectRepository);
                } else if (settingOwner == STAFF) {
                    settingOwner.setSettingOwnerRepository(staffRepository);
                } 
            }
        }

        private OwnerRepositoryInjector(StoreRepository storeRepository,
        	PayrollRepository payrollRepository, StoreStaffRepository storeStaffRepository,
            PayrollStaffRepository payrollStaffRepository) {
            this.storeRepository = storeRepository;
            this.payrollRepository = payrollRepository;
            this.storeStaffRepository = storeStaffRepository;
            this.payrollStaffRepository = payrollStaffRepository;
        }
    }
}

 

1. SettingId 를 반환할 Repository 들을 SettingOwnerRepository 인터페이스를 생성하여 추상화 시켜줍니다.

 

2. Enum 내부의 이너클래스 OwnerRepositoryInjector를 Bean 으로 등록하고, 각 Repository 들과 의존성을 맺어줍니다.

여기서 중요한 점은 이너클래스는 외부에서의 접근을 철저히 막아줍니다.

 

3. Injector 가 빈으로 생성된 후, SettingOwner Enum 에 적절할 Repository 를 각각 할당해줍니다.

 

이로서 Service에 있던 구현이 Enum 내부로 캡슐화 시킬 수 있었습니다.

 

하지만 여전히 코드의 복잡성이 남아있습니다.

지금부터는 Spring IoC 와 PSA 를 활용하여 코드를 조금 더 개선해보겠습니다.

 

 

public interface SettingOwnerRepository {
  SettingOwner getType();
  String findMapperId();
}

public interface CompanyRepository implements SettingOwnerRepository {
   @Overide
   default SettingOwner getType() {
       return SettingOwner.COMPANY;
   }
}
public interface ProjectRepository implements SettingOwnerRepository {
   @Overide
   default SettingOwner getType() {
       return SettingOwner.PROJECT;
   }
}
public interface StaffRepository implements SettingOwnerRepository {
   @Overide
   default SettingOwner getType() {
       return SettingOwner.STAFF;
   }
}

public enum SettingOwner {
    COMPANY,
    PROJECT,
    STAFF

    @Setter
    private SettingOwnerRepository settingOwnerRepository;

    public String findMapperId(String ownerId) {
        return settingOwnerRepository.findMapperId(ownerId);
    }

    @Component
    private static class OwnerRepositoryInjector {
        private Map<SettingOwner, SettingOwnerRepository> repositoryMap = new HashMap<>();
		
        @PostConstruct
        public void init() {
            for (SettingOwner settingOwner : EnumSet.allOf(SettingOwner.class)) {
                settingOwner.setSettingOwnerRepository(repositoryMap.get(settingOwner));
            }
        }

        private OwnerRepositoryInjector(List<SettingOwnerRepository> repositories) {
            for (SettingOwnerRepository repo : repositories) {
                repositoryMap.put(repo.getType(), repo);
            }
        }
    }
}

1. SettingOwnerRepository 인터페이스에 getType() 메서드를 정의하고,

해당 인터페이스를 상속받는 실제 주입받을 인터페이스에 오버라이드하여 default 메서드로 알맞은 타입을 반환하도록 구현합니다.

 

2. Injector 의 필드를 Type 을 키 값으로 갖는 맵으로 변경한 후, 생성자를 List<SettingOwnerRepository> 타입을 받도록 변경하여, SettingOwnerRepository 타입의 모든 빈을 주입받고 1번에서 정의한 getType() 메서드를 키로하여 맵을 채워줍니다.

 

3. Injector 빈이 생성된 후 @PostConstruct 를 이용하여 각 Type 에 해당하는 빈을 맵에서 꺼내서 주입해줍니다.

 

복잡한 switch 구현문이 내부로 완전히 감추어짐과 함께, 코드도 훨씬 가독성이 좋게 변경되었습니다.