의존성주입 개선시도

뾰족머리삼돌이·2024년 3월 25일
0

Spring

목록 보기
7/11

기존 의존성주입

애노테이션을 공부하다보니 갑자기 과거의 기억이 스쳤다
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개다

프로젝트 당시에도 가독성의 문제로 개선하고 싶다는 생각을 했었다
애노테이션을 배우다보니 개선방법이 생각나서 한번 작성해봤다

애노테이션을 이용한 개선

동작방식은 다음과 같다

  1. 주입받는 클래스 선언부에 애노테이션 설정
  2. 설정한 애노테이션에 주입받을 클래스목록 작성
  3. 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) 을 입력하면
InjectionHelpervalues()를 확인해서 빈을 가져온다


이 방법을 이용하면 기존코드는 아래와 같이 바뀔 것이다

@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를 세분화하여 관리하는 것이 아닐까 라는 생각이 들었다.

0개의 댓글