소프트웨어의 개발 원칙 중 SOLID 원칙 중 인터페이스 분리 원칙(ISP)를 따르기 위해 인터페이스와 구현 클래스를 분리하여 코드를 구조화하고 의존성 주입을 쉽게한다.
인터페이스는 서비스의 기능을 명시하고, 구현 클래스는 실제 동작할 기능을 구현한다. 이를 통해 다양한 구현체를 사용하고 확장함으로써 코드의 유연성과 확장성이 향상된다.
예를 들어 Service 인터페이스와 그에 해당하는 ServiceImpl 클래스를 작성한다고 가정해보자.
public interface UserService {
User getUserById(Long id);
List<User> getAllUsers();
// 필요한 메서드 선언
}
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long id) {
// userRepository를 사용하여 데이터 접근 로직 구현
// ...
}
@Override
public List<User> getAllUsers() {
// userRepository를 사용하여 데이터 접근 로직 구현
}
}
이렇게 UserService 인터페이스와 구현체 클래스인 UserServiceImpl를 구현하면 UserService 인터페이스를 사용해서 구현체인 UserServiceImpl을 주입받을 수 있다.
@Autowired는 자동적으로 의존관계를 주입시켜주는 어노테이션이다. Component Scan을 통해 @Component로 객체를 스프링 빈에 등록시키면, 등록된 빈에서 필요한 의존관계를 설정한다.
public interface UserService {
// 메서드 선언
}
@Service
public class UserServiceImpl1 implements UserService {
// 메서드 구현
}
@Service
public class UserServiceImpl2 implements UserService {
// 메서드 구현
}
@Controller
public class UserController {
@Autowired
private UserService userService;
}
우선적으로 해당 Type으로 해당되는 Bean을 찾는 방식이다.
자동으로 의존관계를 주입할 경우 해당 Type의 bean이 하나일 경우에는 해당 Bean이 자동으로 주입이 되지만, 구현체를 여러개 만들 경우 해당 Type의 bean이 여러 개가 되기 때문에 해당 Type만으로 주입하려는 bean을 찾을 수 없어 오류가 발생하게 된다.
이를 해결하는 방법은 의존성 주입을 받는 필드 이름은 구현체의 이름으로 명시하는 것이다.
@Autowired는 우선적으로 Type으로 bean을 찾지만, 찾지 못할 경우 필드 명으로 bean을 찾는 특징이 있다.
필드의 이름을 실제 원하는 구현체와 일치시킨다면 여러 개의 bean을 찾을 때 해당 구현체의 이름을 가진 bean을 주입할 수 있게 된다.
하지만 필드의 이름과 구현체의 이름을 일치시키는 방식은 명시적인 방식은 아니기 때문에, 가독성과 유지보수성에서 좋지 않은 방식이다, 또한 구현체의 이름을 변경하게 된다면 해당 필드의 이름도 변경해야한다.
@Primary는 우선순위를 지정하는 방식이다.
public interface UserService {
// ...
}
@Service
@Primary
public class UserServiceImpl1 implements UserService {
// ...
}
@Service
public class UserServiceImpl2 implements UserService {
// ...
}
위와 같이 UserService 인터페이스의 구현체들 중 @Primary를 설정한 구현체는 기본 주입 구현체가 된다.
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// ...
}
그러면 여러 개의 UserService 빈이 있더라도 @Primary가 붙은 구현체가 기본적으로 주입되는 방식이다.
실무에서 많이 사용하는 방법이라고 한다.
스프링 컨테이너가 여러 개의 빈을 찾았을 때, 추가적으로 판단할 수 있는 정보를 명시적으로 주는 원리라고 이해하면 된다.
해당 구현체의 코드에 @Qualifier(”찾을 이름”)으로 작성하게 되면, 이후 구현체를 주입할 때 해당 이름으로 주입할 수 있게 된다.
public interface UserService {
// ...
}
@Service
@Qualifier("userServiceImpl1")
public class UserServiceImpl1 implements UserService {
// ...
}
@Service
@Qualifier("userServiceImpl2")
public class UserServiceImpl2 implements UserService {
// ...
}
@Controller
public class UserController {
private final UserService userService;
public UserController(@Qualifier("userServiceImpl1") UserService userService) {
this.userService = userService;
}
// ...
}
위와 같이 userServiceImpl1 구현체를 주입받고 싶다면 해당 Qualifer에서 지정한 이름을 통해 원하는 구현체를 주입할 수 있게 된다.
Qualifer를 사용할 때 앞서 말한 대로 등록한 이름을 그대로 입력해주어야한다. 오타가 발생한다해도 컴파일 단계에서 걸러주지 않는다. 별도의 커스텀한 어노테이션을 생성하여, 오타나 잘못된 구현체 지정 등의 오류를 컴파일 시점에서 확인하는 방식으로 응용할 수 있다.
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface MainUserService{
}
위에 예시 코드는 커스텀한 어노테이션 @MainUserService를 생성한 코드이다. @Qualifer어노테이션을 상속받고, @Retention(RetentionPolicy.RUNTIME)를 설정해서 런타임까지 어노테이션 정보가 유지되도록 했다. 이 커스텀한 Qualifer 어노테이션을 사용하여 주입하려는 구현체를 지정할 수 있다.
@Service
@MainUserService
public class UserServiceImpl1 implements UserService {
// ...
}
@Service
public class UserServiceImpl2 implements UserService {
// ...
}
이와 같이 내가 주로 사용할 구현 클래스에 @MainUserService을 붙인다면, 이후 해당 빈을 주입받고자 할 때 해당 어노테이션을 사용하여 주입받을 수 있다.
@Controller
public class UserController {
private final UserService userService;
public UserController(@MainUserService UserService userService) {
this.userService = userService;
}
// ...
}
이렇게 코드를 작성하면 컴파일러가 @MainUserService 어노테이션이 지정된 UserServiceImpl1를 주입하는 것을 보장한다. 이러한 방식은 코드의 가독성이나 명확성을 높이고, 잘못된 주입을 방지한다.
의존성 주입과 관련해서 공부하면서 궁금한 점이 생겨서 질문합니다!!!
조금 극단적인 예시이지만... 만약에 UserController1,2,3,4,…100개 있는데 50개는 구현체1을 쓰고 싶고 50개는 구현체2를 쓰고 싶으면 @Qualifier 를 사용해서 다 명시를 해줘야 하잖아요...? 변경사항이 발생했을 때도 그렇고요... 그러면 어떤 클래스를 생성할지에 대한 책임을 외부에 두고 UserController는 어떤 구현체를 생성할지 신경을 안 쓰게 해준다는 의존성 주입의 이점이 쓸모없어지는게 아닌가요??