애노테이션을 공부하다보니 갑자기 과거의 기억이 스쳤다
Spring에서 의존성 주입을 받다보면 점점 길어져서 불편했던 기억이다
롬복을 이용한 의존성주입은 간편하지만 의존성이 많아질수록 차지하는 부피가 커지는 문제가 있었다
@Service @Slf4j
@Transactional
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final PasswordEncoder passwordEncoder;
private final AwsS3Uploader awsS3Uploader;
private final UserRepository userRepository;
private final MemberRepository memberRepository;
private final TeamRepository teamRepository;
private final AttachRepository attachRepository;
private final ChargeRepository chargeRepository;
private final FollowRepository followRepository;
private final FundingRepository fundingRepository;
private final DonationRepository donationRepository;
private final WishRepository wishRepository;
private final PaymentRepository paymentRepository;
private final GiftRepository giftRepository;
private final UserBadgeRepository userBadgeRepository;
//...
주입받는 Repository만 해도 12개다
프로젝트 당시에도 가독성의 문제로 개선하고 싶다는 생각을 했었다
애노테이션을 배우다보니 개선방법이 생각나서 한번 작성해봤다
동작방식은 다음과 같다
- 주입받는 클래스 선언부에 애노테이션 설정
- 설정한 애노테이션에 주입받을 클래스목록 작성
2.에 작성한 클래스 목록에 해당하는 인스턴스 획득
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectionHelper {
Class<? extends Injectable>[] value();
}
먼저, 사용할 애노테이션을 생성했다. 명칭은 주입을 도와주니까 InjectionHelper로 작성했다
value()에 주입받을 클래스목록을 입력받았다
Injectable라는 인터페이스를 이용해서 대분류 역할을 만들었다
@Service
public class AService implements Injectable {
// ...
}
@Service
public class BService implements Injectable {
// ...
}
@RestController
@InjectionHelper({AService.class, BService.class})
@RequiredArgsConstructor
public class HelloController {
// ...
}
애노테이션 설정하며 주입받을 수 있는 클래스목록을 작성했다
이제 목록에 있는 클래스 빈을 얻어내야한다
이를 위해서는 현재 클래스내의 @InjectionHelper에 접근해야하고, 얻어낼 클래스 타입을 알아야한다
또한, 빈을 얻어내기위해 ApplicationContext에 접근해야한다
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectionHelper {
Class<? extends Injectable>[] value();
@Component
@RequiredArgsConstructor
class Finder{
private final ApplicationContext context;
public <T extends Injectable> T getBeanWithType(Class<?> source, Class<T> target){
InjectionHelper helper = source.getAnnotation(InjectionHelper.class);
if(helper == null){
throw new RuntimeException("부적절한 source 오류");
}
Class<? extends Injectable>[] values = helper.value();
for(Class<? extends Injectable> service : values){
if(service.equals(target)){
return context.getBean(target);
}
}
throw new RuntimeException("부적절한 target 오류");
}
}
}
@InjectionHelper는 빈을 주입받는 모든 클래스를 대상으로 하기때문에 애노테이션에 로직을 작성했다
또한 애노테이션에는 body를 가진 메서드( default, static 포함 )를 생성할 수 없기때문에
정적 멤버 클래스인 Finder를 만들었다
구현한 코드구성은 아래와 같다
Finder.getBeanWithType()는Injectable를 구현하는 T를 반환한다
이는target의 클래스를 의미하며,source를 통해 검사할 클래스를 입력받는다
source에 있는InjectionHelper를 검사해서 클래스 목록을 얻은 다음
target과 동일한게 있으면ApplicationContext를 이용해서 해당 클래스타입의 빈을 반환하는 형식이다
@RestController
@InjectionHelper({AService.class, BService.class})
@RequiredArgsConstructor
public class HelloController {
private final InjectionHelper.Finder serviceFinder;
@GetMapping("/hello")
private ResponseEntity<String> hello(){
AService service = serviceFinder.getBeanWithType(this.getClass(), AService.class);
return ResponseEntity.ok(service.Hello());
}
}
실사용을 위해서는 InjectionHelper.Finder를 주입받아야한다
이후, 해당 빈을 이용하여 현재 클래스(this.getClass()) 와 원하는 Bean타입(AService.class) 을 입력하면
InjectionHelper의 values()를 확인해서 빈을 가져온다
이 방법을 이용하면 기존코드는 아래와 같이 바뀔 것이다
@Service @Slf4j
@Transactional
@InjectionHelper({UserRepository.class, MemberRepository.class, TeamRepository.class
, AttachRepository.class, ChargeRepository.class, FollowRepository.class
, FundingRepository.class, DonationRepository.class, WishRepository.class
, PaymentRepository.class, GiftRepository.class, UserBadgeRepository.class})
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final PasswordEncoder passwordEncoder;
private final AwsS3Uploader awsS3Uploader;
//...
}
body가 간결해져서 기존보다 좀 더 서비스로직에 집중이 가능하다
대신 모든 메서드에서 서비스를 얻어내는 코드를 작성해야한다는 단점도 있다
각 메서드의 시작시점마다 리플렉션을 사용하는 것은 불편하고, 성능상 문제가 있다고 생각했다
따라서, @PostConstruct를 이용하여 Map에 의존성 목록을 저장하는 형식으로 개선을 해봤다
public Map<Class<? extends Injectable>, Injectable> getBeansWithType(Class<?> source){
Objects.requireNonNull(source);
InjectionHelper helper = source.getAnnotation(InjectionHelper.class);
if(helper == null){
throw new RuntimeException("부적절한 source 오류");
}
Class<? extends Injectable>[] values = helper.value();
Map<Class<? extends Injectable>, Injectable> map = new HashMap<>();
for(Class<? extends Injectable> service : values){
Injectable bean = context.getBean(service);
map.put(service, bean);
}
return Collections.unmodifiableMap(map);
}
Collections.unmodifiableMap를 이용하여 불변 Map을 반환한다
@Service @InjectionHelper({ProductDao.class})
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final InjectionHelper.Finder finder;
private Map<Class<? extends Injectable>, Injectable> beans;
@PostConstruct
private void init(){
this.beans = finder.getBeansWithType(this.getClass());
}
@Override
public ProductResponseDto getProduct(Long number) {
ProductDao productDAO = (ProductDao)beans.get(ProductDao.class);
Product product = productDAO.selectProduct(number);
ProductResponseDto responseDto = new ProductResponseDto(product);
return responseDto;
}
// ...
}
실제 사용에서는 @PostConstruct로 의존성 Map을 세팅해두고 꺼내쓰는 방식으로 개선해봤다
리플렉션과 애노테이션을 배우면서 나름의 해결방법이라 생각하고 시도한 내용이었다.
하지만, 결국 문제의 원인은 한 Service에 너무 많은 책임을 할당했기 때문이라고 생각한다.
이상적인 해결방법은 Service를 세분화하여 관리하는 것이 아닐까 라는 생각이 들었다.