스프링 프레임워크는 객체 지향 프로그래밍에서 핵심적인 디자인 패턴인 의존성 주입(DI, Dependency Injection)을 지원합니다.
DI는 객체 간의 의존 관계를 개발자가 아닌 프레임워크가 자동으로 주입해주는 방식으로, 코드의 결합도를 낮추고, 유연성과 테스트 용이성을 높여줍니다.
이번 글에서는 서비스 계층(Service Layer)을 DI를 활용해 설계하는 방법과, DI의 주요 방식인 @Autowired 필드 주입과 생성자 주입의 차이점을 알아보겠습니다.
의존성 주입 DI(Dependency Injection)란?
의존성 주입(Dependency Injection)은 클래스가 다른 클래스에 의존할 때, 직접 생성하지 않고 외부에서 필요한 객체를 주입받는 방식입니다.
이를 통해 객체 간의 결합도를 낮추고, 유연한 설계를 할 수 있습니다.
스프링에서는 DI를 통해 애플리케이션의 객체를 연결해주며, 객체 간의 의존성을 컨테이너가 관리합니다.
이렇게 되면 개발자는 객체를 직접 생성하거나 초기화할 필요 없이, 객체를 주입받아 사용할 수 있습니다.
DI의 주요 이점
- 유연성: 클래스 간의 결합도를 줄이고, 객체 간의 관계를 쉽게 변경할 수 있습니다.
- 테스트 용이성: Mock 객체나 테스트용 구현체를 주입해 단위 테스트를 쉽게 할 수 있습니다.
- 코드 가독성: 명확한 객체 간 의존 관계를 보여줍니다.
- 낮은 결합도: 객체 간의 의존성을 외부에서 주입하므로, 코드 변경 시 영향 범위가 줄어듭니다.
- 재사용성: 객체의 생성을 분리하여, 코드 재사용과 확장성이 높아집니다.
서비스 계층(Service Layer) 설계
스프링에서 서비스 계층은 비즈니스 로직을 담당하는 중요한 부분입니다.
이 계층은 일반적으로 컨트롤러와 데이터 접근 계층(DAO or Repository) 사이에서 데이터 처리를 담당합니다.
서비스 계층을 설계할 때, 각 클래스 간 의존성을 효율적으로 관리하기 위해 DI를 적용할 수 있습니다.
아래의 예시는 스프링의 DI를 활용해 서비스 계층을 설계하는 방법을 보여줍니다.
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입 방식
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));
}
public void createUser(User user) {
userRepository.save(user);
}
}
위 코드에서 UserService는 UserRepository에 의존하고 있습니다.
UserRepository는 데이터베이스와 상호작용하는 데이터 접근 계층(Repository)입니다.
스프링의 DI 기능을 이용해 UserRepository 객체를 직접 생성하지 않고, 생성자 주입을 통해 전달받습니다.
스프링 컨테이너가 자동으로 적절한 UserRepository 구현체를 찾아 주입해주기 때문에, 개발자는 객체 간의 의존 관계를 일일이 신경 쓰지 않아도 됩니다.
@Autowired와 생성자 주입의 차이점
스프링에서 의존성 주입을 구현하는 방식은 여러 가지가 있습니다. 그 중 가장 흔히 사용되는 두 가지 방식은 @Autowired 필드 주입과 생성자 주입입니다. 각각의 방식은 DI를 처리하는 방식이 다르며, 그에 따른 장단점도 존재합니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
@Autowired를 사용한 필드 주입은 스프링이 해당 필드에 필요한 의존성을 자동으로 주입해주는 방식입니다. 코드는 간결하지만, 다음과 같은 단점들이 존재합니다.
- 테스트 용이성 저하: 필드 주입 방식은 테스트에서 주입받는 객체를 교체하기 어려워, Mock 객체를 이용한 단위 테스트가 복잡해집니다.
- 불변성 미보장: 객체 생성 후 의존성이 주입되므로, 해당 의존성이 변경될 가능성이 있으며, 불변 객체를 만들기 어렵습니다.
- 순수 자바 객체로의 사용 불가: 필드 주입은 스프링 컨테이너에 의존하기 때문에, 스프링 프레임워크 없이 순수 자바 객체로는 사용할 수 없습니다.
생성자 주입
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입 방식
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
생성자 주입은 의존성을 생성자를 통해 주입받는 방식으로, 스프링 4.3 이후부터는 생성자가 하나만 존재할 경우 @Autowired 애너테이션을 생략할 수 있습니다. 이 방식은 다음과 같은 장점을 제공합니다.
- 불변성 보장: final 키워드를 사용하여 객체의 의존성을 변경할 수 없게 만들고, 생성 시점에 모든 의존성이 주입되므로 객체가 완전하게 초기화됩니다.
- 테스트 용이성: 생성자를 통해 주입되기 때문에 테스트 시에도 Mock 객체를 쉽게 전달할 수 있어 단위 테스트 작성이 용이합니다.
- DI 프레임워크에 덜 의존적: 생성자 주입은 스프링 외부의 순수 자바 환경에서도 사용할 수 있어, 프레임워크 의존성을 줄일 수 있습니다.
@Autowired와 생성자 주입의 차이점 요약
@Autowired 필드 주입 | 생성자 주입 | |
주입 시점 | 객체가 생성된 후 주입 | 객체 생성 시 주입 |
불변성 보장 | 불변성 보장 어려움 | 불변성 보장 가능 |
테스트 용이성 | 테스트에서 Mock 주입 어려움 | 테스트 시 주입 용이 |
코드 간결성 | 상대적으로 간결 | 생성자 작성 필요 |
DI 프레임워크 의존성 | 프레임워크에 강하게 의존 | 덜 의존적 |
Lombok을 활용한 의존성 주입 간소화
의존성 주입을 더 효율적으로 구현하기 위해, 스프링에서 널리 사용하는 Lombok 라이브러리를 활용할 수 있습니다. Lombok은 반복적인 코드 작성을 줄이고, 가독성을 높여주는 라이브러리로, 특히 생성자 주입 시 유용하게 사용할 수 있습니다.
Lombok을 사용한 생성자 주입
Lombok의 @RequiredArgsConstructor 애너테이션을 사용하면, final 필드를 매개변수로 받는 생성자를 자동으로 생성해줍니다. 이를 통해 생성자 주입을 간결하게 구현할 수 있습니다.
@Service
@RequiredArgsConstructor // Lombok이 자동으로 생성자 생성
public class UserService {
private final UserRepository userRepository;
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));
}
public void createUser(User user) {
userRepository.save(user);
}
}
Lombok을 사용하면 생성자 코드를 작성할 필요 없이, final 필드만 선언해주면 Lombok이 자동으로 생성자를 만들어줍니다. 이 방식은 코드의 가독성을 높이고 유지보수를 더 쉽게 만들어줍니다.
Lombok의 장점
- 간결성: 생성자 코드를 자동으로 생성해주므로, 반복적인 코드 작성을 줄일 수 있습니다.
- 불변성 보장: final 키워드를 사용하여 의존성을 주입받기 때문에, 객체가 불변성을 가지도록 설계할 수 있습니다.
- 테스트 용이성: Lombok으로 생성된 생성자는 테스트 환경에서도 유연하게 사용할 수 있습니다.
결론
스프링 애플리케이션에서 의존성 주입(DI)을 활용하면 객체 간의 결합도를 낮추고, 코드의 유연성과 테스트 용이성을 크게 향상시킬 수 있습니다. @Autowired 필드 주입은 간편하지만, 생성자 주입은 불변성을 보장하고 테스트 환경에서 더 유리합니다.
또한, Lombok을 활용하면 생성자 코드를 더욱 간결하게 작성할 수 있으며, 코드의 가독성과 유지보수성을 높일 수 있습니다. 스프링 프로젝트에서 DI 방식과 함께 Lombok을 적절히 활용하면, 보다 효율적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
Lombok을 사용하면 코드 작성 시간을 줄이면서도 의존성 관리가 용이해지므로, 특히 생성자 주입 방식과 함께 사용하면 큰 시너지 효과를 기대할 수 있습니다.
'Spring > Spring' 카테고리의 다른 글
Spring Boot에서 @Value로 주입된 값을 Static 변수로 사용하기 (0) | 2024.10.11 |
---|---|
[MSA] Hystrix 와 resilience4j를 통한 circuit breaker (0) | 2023.08.30 |
엑셀 다운로드 에러 발생 해결 - FontConfiguration에서 NullPointerException 발생 (0) | 2023.04.05 |