도메인 협력관계 - 기획자도 같이 보는
회원클래스 다이어그램 -> 인터페이스, 구현체 정적
회원객체 다이어그램 -> 실제 동적시간에 확정된 객체들 동적임 실행해봐야 알 수 있음.
회원도메인 설계의 문제점
- 이 코드의 설계상 문제점은??
- 다른 저장소로 변경할 때 OCP 원칙 준수하고있는지?
- DIP를 잘 지키고 있는지?
- 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음.
1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
2. 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
Test
@DisplayName -> 테스트시 이름 보여줌.
테스트시 성공테스트뿐아니라 실패테스트도 해봐야함.
public class OrderServiceImpl implements OrderService{
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final MemberService memberService = new MemberServiceImpl();
할인 정책을 변경할 때 순수 자바코드로 구현시 주문서비스 구현체에 할인 정책을 직접 바꿔야함.
문제점 발견
역할 분리 충실하게 분리 - o
다형성 활용, 인터페이스와 구현 객체 분리 o
OCP, DIP 같은 객체지향 원칙 준수? -> 그렇게 보이지만 X
DIP : 주문서비스 클라이언트(OrderService Impl)은 DiscountPolicy 인터페이스에 의존하며 잘 지킨 것같지만....
- 인터페이스에도 의존하지만 구체클래스에도 의존하고 있다.
- 추상: Discount Policy
- 구체: FixDiscountPoliciy, RateDiscountPolicy
OCP: 변경 없이 확장..? 할인 정책기능을 바꾸는데 OrderServiceImpl의 코드가 수정됨.
해결해보자!
public class OrderServiceImpl implements OrderService{
private DiscountPolicy discountPolicy;
private final MemberService memberService = new MemberServiceImpl();
인터페이스만 의존하게 변경
but 오류남! 왜냐하면 인터페이스의 구현체가 지정되어있지 않기 때문이다.
그렇다면 어떻게 해야하지.....
누군가 구현체를 대신 생성하고 넣어주면 될것같은디...?
관심사의 분리
공연기획자가 배우를 섭외해야지 배우가 배우를 섭외하는 건 좀...;
AppConfig -> 공연기획자의 역할
구현객체를 생성하고, 연결하는 책임을 가지는 연결 클래스
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new FixDiscountPolicy(), new MemoryMemberRepository());
}
}
AppConfig 가 실제 동작에 필요한 객체를 생성하고 생성자를 통해 주입시켜준다.
MemberServiceImpl은 인터페이스만 가지고 있음.
의존관계 고민은 외부(AppConfig)에게 맡기고 실행에만 집중하면됨.
관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
정리
AppConfig를 통해서 관심사를 확실하게 분리했다.
OrderServiceImpl은 기능을 실행하는 책임만 지면 됨.
Appconfig 리펙토링
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new FixDiscountPolicy(), new MemoryMemberRepository());
}
}
현재 Appconfig는 중복도 있고 역할에 따른 구현이 잘 안보임.
public class AppConfig {
public MemberService memberService(){//멤버서비스 역할
return new MemberServiceImpl(memberRepository());
}
private MemoryMemberRepository memberRepository() {//리포지토리 역할
return new MemoryMemberRepository();
}
public OrderService orderService(){//order서비스 역할
return new OrderServiceImpl(discountPolicy(), memberRepository());
}
public DiscountPolicy discountPolicy(){//DiscountPolicy 역할
return new FixDiscountPolicy();
}
}
역할과 구현클래스가 한눈에 들어옴.
Appconfig의 코드만 바꿔주면 적용됨.
SRP 단일 책임 원칙
관심사를 분리
구현객체를 생성하고 연결하는 책임은 AppConfig 가 담당
클라이언트 객체는 실행하는 책임만 담당
DIP 의존관계 역전 원칙
위의 예제는 인터페이스도 의존했지만 구체클래스도 의존했음.
따라서 의존관계를 주입함으로써 인터페이스만 의존하게 만듦.
OCP
DiscountPolicy를 바꿨을 때 클라이언트의 코드는 변경하지 않아도됨.
소프트웨어적 요소를 새롭게 확장해도 사용영역의 변경은 닫혀 있다.
IoC, DI, 컨테이너
IoC, 제어의 역전
기존 프로그램 -> 구현 객체가 스스로 필요한 객체를 만들고 연결하고 실행했음. 구현객체가 프로그램의 제어흐름을 조종. 개발자입장에서는 자연스러운 흐름.
but, Appconfig의 등장 이후에는 구현 객체는 로직 실행 담당만 함. 프로그램의 제어흐름은 Appconfig가 가진다. ex) 구현객체들은 인터페이스를 호출하지만 어떤 객체가 호출될지는 모른다.
각각의 구현체(ServiceImp)은 인터페이스만 가지고 실행. 사실 다른 객체가 들어올수도있지만 묵묵히 자신의 로직만을 실행한다. 이렇듯 프로그램의 제어흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어역전(IoC)라고 한다.
프레임워크 vs 라이브러리
- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그건 프레임워크(ex. Junit)
- 반면 내가 작성한 코드가 직접 제어흐름을 담당하면 그것은 라이브러리
DI, 의존관계 주입
- OrderServiceIml은 어떤 DiscountPolicy가 실제 구현 객체로 들어올지 모른다.
- 의존관계는 정적인 클래스 의존관계와 동적인 객체 의존관계 를 분리해서 생각해야 한다.
정적인 클래스 의존관계
- 코드만 보고 분석, 판단할 수 있음.
- ex) OrderServiceImpl의 경우 MemberRepository, DiscountPolicy를 의존한다는 것을 알 수 있다. 하지만 클래스 의존관계로는 실제로 어떤 객체가 주입될지 알수 없다.
DI
- 애플리케이션 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 의존관계가 연결되는 것을 의존관계 주입.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다,
IoC컨테이너, DI컨테이너
Appconfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC컨테이너 혹은 DI 컨테이너라고 한다.
스프링 컨테이너
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
ApplicationContext : 스프링 컨테이너
기존에는 AppConfig를 사용해서 직접 객체를 만들고 DI를 했지만, 이제부터는 스프링 컨테이너가 이것을 해준다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){//멤버서비스 역할
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {//리포지토리 역할
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){//order서비스 역할
return new OrderServiceImpl(discountPolicy(), memberRepository());
}
@Bean
public DiscountPolicy discountPolicy(){//DiscountPolicy 역할
return new RateDiscountPolicy();
}
}
- @Configuration이 붙은 AppConfig를 구성정보로 사용.
- @Bean 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록. 이렇게 등록된 객체를 스프링 빈이라고 한다.
- 스프링 빈은 @Bean이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용.(이름을 바꿀 순 있다.) 이전에는 개발자가 Appconfig를 사용해서 직접 조회 했지만 이제는 스프링컨테인를 이용해서 Bean객체를 찾아야한다.
- ->ac.getBean()메서드를 통해 찾을 수 있다.
기존에는 개발자가 직접 자바코드로 모든것을 했다면 이제는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경.
코드가 좀더 복잡해진것같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까??....
'Spring > 스프링 원리 - 기본편' 카테고리의 다른 글
[스프링-기본] 섹션 6. 컴포넌트 스캔 (0) | 2023.01.24 |
---|---|
[스프링-기본] 섹션5. 웹 애플리케이션과 싱글턴 (0) | 2023.01.23 |
[스프링-기본] 섹션4. 스프링 컨테이너와 스프링 빈 (0) | 2023.01.22 |
[스프링-기본] 섹션1. 객체 지향 설계와 스프링 (0) | 2023.01.18 |