스프링 빈에 대해 공부하다가 의존성 관리와 implements의 차이가 뭔지 궁금해졌다.
그래서 이에 대해 정리해보고자 한다.
우리는 인터페이스를 구현할 때 implements 키워드를 사용한다.
만약 UserRepository가 인터페이스라면 그 안에 정의된 추상 메서드를 UserService에서 직접 구현해야 한다.
interface UserRepository {
void save(User user);
}
class UserService implements UserRepository {
@Override
public void save(User user) {
// 저장 로직 직접 구현
}
}
이 경우에는 UserService는 UserRepository 자체를 구현하는 클래스가 된다. 즉, 둘은 같은 책임을 가지게 되기에 계층 분리가 무너지게 된다.
스프링에서는 일반적으로 서비스 계층과 레포지토리 계층을 분리한다. 위 예시로 설명해보자면, UserService는 UserRepository를 직접 구현하지 않고 필요로 하기만 한다. 스프링에서는 스프링 컨테이너가 UserRepository 구현체를 대신 생성해 UserService에 주입해준다.
// 서비스어
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(User user) {
userRepository.save(user);
}
}
// 레포지토리
@Repository
public class JpaUserRepository implements UserRepository {
@Override
public void save(User user) {
// JPA로 DB 저장
}
}
여기서 UserService 입장에서는 UserRepository의 구현체를 알 필요가 없다. 스프링이 알아서 적절한 객체를 생성하고 주입해주기에 코드가 유연하고 테스트하기 더 쉬워진다.
스프링에서는 @Component, @Service 와 같은 애노테이션이 붙은 클래스를 자동으로 스캔한다. 이렇게 스캔된 클래스들은 스프링 빈이 되어 컨테이너에 등록되고, 스프링 컨테이너가 객체를 생성하고 관리하게 된다.
// 레포지토리로 인식 -> 컨테이너 등록
@Repository
public class JpaUserRepository implements UserRepository {
@Override
public void save(User user) {
// DB 저장 로직
}
}
// 서비스로 인식 -> 컨테이너 등록
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(User user) {
userRepository.save(user);
}
}
여기서 생성자 주입 원리에 대해 알아보도록 하자.
UserService 스캔UserService의 생성자를 확인하고 필요한 타입(UserRepository)이 무엇인지 확인UserRepository 타입의 빈을 찾음UserService 찬음UserService 입장에서는 UserRepository의 구현체를 알 필요가 없어짐[UserService] ---UserRepository---> [JpaUserRepository]
↑ 스프링 컨테이너가 생성자에 넣어줌
즉, 스프링이 인터페이스 타입과 구현체를 매칭해주기에 우리가 직접 new JpaUserRepository()를 호출할 필요가 없어진 것이다.