대략 이런 구조의 여러개의 도메인 루트들이
여러 셋팅 값들의 집합인 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 구현문이 내부로 완전히 감추어짐과 함께, 코드도 훨씬 가독성이 좋게 변경되었습니다.
'Spring' 카테고리의 다른 글
도메인 객체에서 Bean 객체의 기능을 필요로 해 수행한 코드 개선기 (0) | 2022.05.25 |
---|---|
SpringBoot + Redis 를 이용한 글로벌 캐시 (0) | 2022.03.14 |
TestContainer 로 테스트환경 셋업하기 (0) | 2022.03.14 |
Spring Bean LifeCycle 스프링 빈 생명주기 (0) | 2022.01.24 |