[Spring Boot] 구조 분석 (5) - 두개 이상 Bean을 등록시 문제 및 해결
- 지난 시간
안녕하세요. 지난 시간에는 의존관계 주입 방식에 대해 알아봤습니다. 혹시 놓치고 오신 분들은 학습하고 오시는 걸 추천드리겠습니다.
[Spring Boot] 구조 분석 (4) - 의존관계 주입 방식
- 개요
이번 시간에는 Spring Container의 두 개 이상의 Bean을 등록 시 발생하는 문제점에 대해 알아보고 해결방법에 대해 알아보겠습니다.
Spirng Boot로 개발을 하게 되면 여러 컨트롤러와 서비스 그리고 리포지토리를 만들게 됩니다.
저도 엄청난 양의 컨트롤러와 서비스 그리고 리포진토리를 만들어 프로젝트를 진행해 본 경험은 없습니다. (사실... 큰 규모로 프로젝트를 진행하게 되면... MSA로 전환을 많이 고민하게 됩니다. -> MSA의 대한 포스팅은 다음에 하겠습니다.)
하지만 Spring Boot와 같은 DI프레임워크를 접하시는 분들에게는 한 번쯤은 꼭 접하는 문제점입니다.
지난 시간부터 쭉~ 만들어 놓은 UserRepositoryInterface를 활용해서 예시를 진행하겠습니다.
- UserRepositoryInterface
UserRepositoryInterface 코드는 단순합니다.
package com.ryan.spring_boot_rest_api.repository;
import com.ryan.spring_boot_rest_api.domain.User;
import java.util.Collection;
public interface UserRepositoryInterface {
int save(int id, String name);
User findByUser(int id);
Collection<User> findAllByUser();
User update(int id, String name);
Boolean delete(int id);
}
하지만 해당 인터페이스를 implements 하는 클래스는 UserMemoryRepository와 UserDiskRepository입니다.
예시를 위해 몇 가지 코드를 수정하겠습니다.
1. AppConfig
@Configuration
public class AppConfig {
@Bean
public UserRepositoryInterface userRepository(){
return new UserMemoryRepository();
}
}
UserMemoryRepository를 Spring Container에 등록하고 있습니다. 이 부분은 잠시... 모두 주석 처리하겠습니다. (동일한 구조를 만들기 위해서입니다.)
2. UserMemoryRepository
@Component로 새롭게 Spring Container의 등록합니다. - AppConfig에서 하는 행위와 동일합니다. -
package com.ryan.spring_boot_rest_api.repository;
import com.ryan.spring_boot_rest_api.domain.User;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class UserMemoryRepository implements UserRepositoryInterface{
Map<Integer, User> memoryDB = new ConcurrentHashMap<>();
...
}
자 이렇게 코드를 변경하면 UserDiskRepository와 동일한 @Component를 통해서 구조를 만들었습니다.
마지막으로 UserService의 코드를 수정하겠습니다.
package com.ryan.spring_boot_rest_api.service;
import com.ryan.spring_boot_rest_api.domain.User;
import com.ryan.spring_boot_rest_api.dto.CreatUserDto;
import com.ryan.spring_boot_rest_api.dto.SuccessResponse;
import com.ryan.spring_boot_rest_api.dto.UpdateUserDto;
import com.ryan.spring_boot_rest_api.repository.UserRepositoryInterface;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserService {
@Autowired
private final UserRepositoryInterface userRepositoryInterface;
...
}
하지만 이렇게 되면 프로젝트를 실행 시 에러가 발생합니다.
Description:
Parameter 0 of constructor in com.ryan.spring_boot_rest_api.service.UserService required a single bean, but 2 were found:
- userDiskRepository: defined in file [/Users/newko/ryan/blog/Spring_Boot_Rest_API/build/classes/java/main/com/ryan/spring_boot_rest_api/repository/UserDiskRepository.class]
- userMemoryRepository: defined in file [/Users/newko/ryan/blog/Spring_Boot_Rest_API/build/classes/java/main/com/ryan/spring_boot_rest_api/repository/UserMemoryRepository.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
여기서 발생하는 Error는 UserService를 생성할 때 @Autowired를 통한 필드 주입 시 주입하려는 Bean이 하나가 아니고 두 개어서 발생하는 문제입니다. (userDiskRepository, userMemoryRepository)
그리고 친절하게 해결방벙에 대해서도 말하고 있습니다.
"userDiskRepository와 userMemoryRepository 중 하나를 @Primary를 선언해서 둘 중 우선순위를 정의하거나 @Qualifier를 사용해서 Rpository를 구분 지어 해결" 하라는 말입니다.
자 그럼 일부로 에러를 발생했으니 이제 해결하는 방법에 대해 알아보겠습니다.
- 해결방법
1. @Autowired 변수명을 통해 해당 주입 클래스 지정
우리가 의존관계를 설정할 때 @Autowired를 선언했습니다. 이때 단순히 해당 인스턴스 이름을 따르지 않고 구체화된 클래스 명을 필드 변수로 선언하면 @Autowired는 변수명의 따라 해당 구체화 클래스를 명시하게 됩니다.
//변경 전
@Autowired
private final UserRepositoryInterface userRepositoryInterface;
//변경 후
@Autowired
private final UserRepositoryInterface userDiskRepository;
변경 후로 코드를 선언하면 아까의 컴파일 에러는 사라지고 해당 userDiskRepository를 연결하게 됩니다.
이유는 @Autowired가 의존관계를 매칭하는 방식에서 알 수 있습니다.
1. 필드 타입
2. 필드 타입을 통한 매칭된 결과가 2개 이상일 때 그다음으로 필드 이름으로 해당 빈을 매칭합니다.
그렇기 때문에 변경 후 코드는 2번째에 해당하기 때문에 컴파일 시 문제가 없습니다.
2. @Primary
@Primary 어노테이션은 해당 구현체의 우선순위를 선어 할 때 사용합니다.
무슨 말이냐 여러 개의 구현체 클래스가 존재할 때 그중에 @Primary로 선언된 클래스를 주입받겠다고 지정합니다.
코드를 통해 설명하겠습니다.
@Service
public class UserService {
private final UserRepositoryInterface userRepositoryInterface;
@Autowired
public UserService(UserRepositoryInterface userRepositoryInterface) {
this.userRepositoryInterface = userRepositoryInterface;
System.out.println("주입된 구현체 조회 -> " + userRepositoryInterface.getClass());
}
...
}
위처럼 코드를 작성하면 당연히 컴파일 에러가 발생합니다. 하지만! @Primary를 우리가 주입받고 싶은 클래스에 선언하게 되면 문제가 해결합니다. 하지만 실제 관계 설정이 잘 이뤄지는지 확인하기 위해서 확인하는 코드를 만들어 보겠습니다.
UserMemoryRepository와 UserDiskRepository 클래스의 기본생성자를 만들고 콘솔 로그 코드를 작성하겠습니다.
@Component
@Primary
public class UserDiskRepository implements UserRepositoryInterface{
Map<Integer, User> diskDB = new ConcurrentHashMap<>();
...
}
@Component
public class UserMemoryRepository implements UserRepositoryInterface{
Map<Integer, User> memoryDB = new ConcurrentHashMap<>();
...
}
위 코드의 차이는 @Primary를 선언하고 안하고의 차이가 있습니다. 실행결과를 확인하면 당연히 UserDiskRepository 구현체가 콘솔창의 나타납니다. 반대로 설정하면 반대의 결과가 나타납니다.
별게 아니지만 증명하는 프로세스를 만드는 습관이 중요하기 때문에 꼭 한 번씩 실행해 보시는 걸 추천드리겠습니다.
3. @Qualifier
@Qualifier는 추가적으로 구분자를 만들어서 구분 지어줍니다. 하지만 구체화된 빈 이름을 변경되는 것은 아닙니다.
바로 예시 코드를 작성하겠습니다. (생성자 주입 @RequiredArgsConstructor 어노테이션을 선언해서 사용하면 @Qualifier가 동작을 하지 않습니다. 꼭 주의!)
동일하게 UserMemoryRepository와 UserDiskMemoryRepository에 @Qualifier를 선언합니다.
@Component
@Qualifier("diskRepository")
public class UserDiskRepository implements UserRepositoryInterface{
...
}
@Component
@Qualifier("memoryRepository")
public class UserMemoryRepository implements UserRepositoryInterface{
...
}
위와 같이 지정한 후 우리가 사용하기 위한 Service 클래스의 코드를 수정하겠습니다. (아까 주의사항을 꼭 잊으시면 안 됩니다.)
@Service
public class UserService {
private final UserRepositoryInterface userRepositoryInterface;
@Autowired
public UserService(@Qualifier("memoryRepository") UserRepositoryInterface userRepositoryInterface) {
this.userRepositoryInterface = userRepositoryInterface;
}
...
}
@RequiredArgsConstructor을 제거하고 기본 생성자 클래스를 선언해 생성자 주입을 진행했습니다.
그러면서 주입받고 싶은 구체화 클래스를 @Qualifier를 통해 지정합니다. 이렇게 되면 위 에러 메시지는 나타나지 않고 memoryRepository를 선언된 @Qualifier를 찾아 관계를 설정합니다.
하지만 주의해야 할 부분이 있습니다. 만약에 @Qualifier에 선언한 이름의 구체화 클래스를 찾지 못하면 어떻게 될까요??
그다음으로는 타입으로 해당 구체화 클래스를 찾습니다.
정리하면 1. @Qualifier를 통한 우선적으로 구체화 클래스를 찾습니다. 2. 찾지 못하면 필드 타입을 통해 찾습니다.
당연히 2번도 안되면... 에러가 발생합니다. NoSuchBeanDefinitionException 에러가 발생합니다.(빈을 찾지 못해서 발생...)
이번시간은 사실 실제 겪기 어려운 문제일 수도 있습니다. 개발을 하면서 추상화에 의존하면서 개발을 하기란 사실 쉽지 않습니다. (복잡성을 더 만드는 행위일 수 있기 때문이죠...)
하지만 한번씩은 만날수 있는 문제와 해결방법에 대한 개념 입니다. 꼭 한번 학습하시는 걸 추천드리겠습니다.
소스 코드
https://github.com/Ryan-Sin/Spring_Boot_Rest_API/tree/v4