생성자 주입이라 함은 클래스의 생성자를 통해 필요한 의존성을 외부에서 전달받는 방식이다.
객체 간의 결합도를 낮추고 테스트를 용이하게 한다.
생성자 주입의 장점에 대해 알아보고 예시 코드를 통해 생성자 주입을 이해해보자.
생성자 주입의 장점
1. 불변성
- 생성자를 통해 주입된 의존성은 주로 final 키워드로 선언하여 불변성을 보장한다.
- 객체가 생성된 후에는 의존성이 변경되지 않으므로 안전한 코드가 된다.
2. 테스트 용이성
- 테스트 시에 원하는 의존성을 직접 주입할 수 있으므로 Mock 객체나 Fake 객체를 주입하는 방식으로 테스트를 할 수 있다.
- 실제 DB 대신 메모리 기반의 가짜 저장소를 주입한다던가 그런 식으로 하면 된다.
@Test
public void testServiceWithMockRepository() {
MyRepository mockRepository = mock(MyRepository.class); // Mock 객체 생성
MyService myService = new MyService(mockRepository); // Mock 주입
myService.performService();
verify(mockRepository).save(); // 메서드가 호출됐는지 확인
}
3. SRP(단일 책임 원칙)
- 생성자 주입은 객체 생성과 비즈니스 로직을 분리한다.
- 단일 책임 원칙을 지키기 위해 AppConfig 같은 설정 클래스를 가지고 하나의 객체의 생성을 담당한다.
- 서비스 객체는 비즈니스 로직만을 책임지게 하여 이를 통해 클래스가 하나의 책임만 갖게 한다.
public class AppConfig {
public MyService myService() {
return new MyService(new MyRepositoryImpl());
}
}
4. 강제성
- 생성자 주입은 객체가 생성될 때 의존성을 반드시 주입받아야 하기 때문에 의존성 누락을 방지할 수 있다.
- 의존성이 없다면 컴파일 타임에 오류 발생
// 의존성 주입을 강제
public class MyService {
private final MyRepository myRepository;
// MyRepository가 없으면 객체를 생성할 수 없음
public MyService(MyRepository myRepository) {
if (myRepository == null) {
throw new IllegalArgumentException("myRepository must not be null");
}
this.myRepository = myRepository;
}
}
5. 다형성
- 생성자 주입을 사용하면 구현체가 아닌 인터페이스에 의존하게 되어 다형성 활용이 가능하다.
- 인터페이스를 통해 다양한 구현체 주입!
public interface DiscountPolicy {
int discount(int price);
}
public class FixDiscountPolicy implements DiscountPolicy {
public int discount(int price) {
return 1000;
}
}
public class RateDiscountPolicy implements DiscountPolicy {
public int discount(int price) {
return price * 10 / 100;
}
}
// 생성자 주입을 통해 정책을 쉽게 교체
public class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
문제 상황 제시 및 해결
그 동안 쇼핑몰의 OrderService라는 인터페이스를 만들어서 주문 서비스 구현을 하는 OrderServiceImpl이라는 구현체를 만들었다.
그러나 할인 정책 인터페이스인 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 줄 알았으나
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
이런 식으로 구현 클래스까지 의존하고 있다는 사실을 알았다.
결국엔 어떤 할인 정책을 적용시킬 때 코드를 확장해서 변경하게 되면 OCP(개방-폐쇄 원칙)를 위반하게 된다.
따라서 AppConfig라는 설정 클래스를 만들어서 애플리케이션의 전체 동작 방식을 구성하기로 했다.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
코드를 잘 짰다고 생각했으나
이렇게 빨간 줄이 뜨는 것을 확인할 수 있었다.
왜 오류가 생기는지 코드를 확인해 봐야겠네 먼저 MemberServiceImpl을 보자.
이 코드는 MemberServiceImpl 클래스 내에서 memberRepository를 직접 new로 초기화하고 있다.
즉, MemberServiceImpl이 생성될 때 내부적으로 어떤 구현체를 사용할지 강하게 결합되어 있는 것이다.
이는 확장성 부족, 테스트의 어려움, 유연성 부족 등의 문제가 생긴다.
생성자는 간단하게 단축키 Alt + Insert를 눌러서 선택해주면 된다.
이제 MemberServiceImpl은 의존성인 MemberRepository를 외부에서 주입받는 방식으로 변경되었다.
단지 MemberRepository 인터페이스만 의존한다.
MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부에서 결정된다.
이제 AppConfig에서 이 의존성을 설정하고 그 의존성을 생성자를 통해 전달해 주는 것이다.
한 번 가서 볼까?
윗 줄의 오류가 사라진 것을 확인할 수 있었다.
밑에도 똑같이 OderServiceImpl에 들어가서 객체 생성을 지우고 생성자를 만들어주면 된다.
이제 오류가 완전히 사라진 것을 확인했다.
이제 각각 기존에 만들었던 memberApp과 OrderApp 클래스에 가서 수정해보자.
AppConfig를 통해 MemberService와 MemberRepository의 객체를 생성하고 주입함으로써 OCP 원칙을 따르고 있는 것을 확인할 수 있다.
OrderApp도 똑같이 고쳐주면
이렇게 된다. 다시 AppConfig 클래스로 돌아가보자.
아까와 다르게 파란색 불이 켜져 있는 것을 확인할 수 있다.
의존성 주입이 잘 이루어졌다는 의미이다.
요약을 해보면
- AppConfig 클래스에서 memberService()와 orderService() 메서드가 각각 MemberServiceImpl과 OrderServiceImpl 객체를 생성하고 그 생성자에 필요한 의존성 주입
- memberService()는 MemoryMemberRepository를 주입받아 MemberServiceImpl 객체 생성
- orderService()는 MemoryMemberRepository와 FixDiscountPolicy를 생성자 주입 방식으로 OrderServiceImpl에 전달
하지만 여기서 끝이 아니다. 더 나은 방향으로 리팩토링을 해보자.
현재 AppConfig를 보면 역할에 따른 구현이 잘 보이진 않는다.
중복된 객체 생성 로직과 결합도를 줄이는 방향으로 리팩토링을 해보자.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
이 코드에서 어떤 부분을 고치는 것이 좋을까?
1. 중복 제거
- 현재 AppConfig 클래스에서 MemoryMemberRepository를 두 번 생성하고 있다.
- 하지만 모든 서비스가 동일한 MemoryMemberRepository를 사용해야 하므로 이 객체를 한 번만 생성하고 공유하도록 해야 한다.
2. 역할별 분리
- 할인 정책 같은 정책들은 따로 메서드로 분리해야 객체 생성 로직을 더 명확하게 관리할 수 잇다.
- 나중에 할인 정책을 변경해야 할 때도 해당되는 메서드만 수정하면 된다.
3. 의존성 주입 방식 통일
- 모든 객체 생성 방식을 통일하고 주입하는 방식을 명확하게 정의하면 유지보수하기 더 용이해진다.
리팩토링한 코드
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
// MemberRepository 객체를 한 번만 생성하여 공유
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// DiscountPolicy도 추상화에 의존하도록 분리
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
// MemberService 생성
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// OrderService 생성
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
memberRepository 메서드를 만들어서 MemoryMemberRepository 객체를 한 번만 생성하도록 했다.
(MemberService와 OrderService에 동일한 인스턴스 주입)
discountPolicy() 메서드를 통해 할인 정책을 분리했다.
나중에 할인 정책이 고정에서 정률 할인으로 변경되거나 더 확장될 때 discountPolicy() 메서드만 수정하면 된다.
다음 시간엔 제대로된 Spring 프레임워크의 기능을 수행해보는 시간을 가질 것이다.
'Backend > Spring Boot' 카테고리의 다른 글
스프링 컨테이너와 스프링 빈 (1) | 2024.10.13 |
---|---|
JUnit5를 활용한 스프링 빈 조회 Test (0) | 2024.10.06 |
[Spring] 스프링 빈(Bean)이란? (1) | 2024.09.10 |
lombok이란? (3) | 2024.09.03 |
MVC 패턴 (63) | 2024.08.18 |