애노테이션을 공부하다보니 갑자기 과거의 기억이 스쳤다
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를 세분화하여 관리하는 것이 아닐까 라는 생각이 들었다.