새로운 할인 정책 개발
RateDiscountPolicy
- 가격에 비례해서 할인을 적용
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP)
return price * discountPercent / 100;
else
return 0;
}
}
→ 실제 적용했을 때, 잘 적용되는지를 확인해야 한다.
새로운 할인 정책 적용과 문제점
할인 정책 적용
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
**private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 해당 부분을 변경**
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
- 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
- 역할과 구현 분리 : O
- 다형성 활용, 인터페이스와 구현 객체 분리 : O
- OCP, DIP와 같은 객체지향 설계 원칙 준수 : X
문제점
- DIP : OrderServiceImpl이 구현체에도 의존을 하고 있으므로, DIP를 지키지 못 함
- OCP : DPI를 만족하지 못 하므로, 기능을 확장해서 변경하는 경우에 코드에 변경에 생기게 된다.
해결책
- DIP를 위반하지 않도록 인터페이스에만 외존하도록 의존관계를 변경
- 기능이 확장돼도 코드에 변경도 생기지 않는다 → OCP도 만족
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
**private DiscountPolicy discountPolicy; // interface에만 의존!!**
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
- discountPolicy에 아무것도 없으니, 코드가 동작하지 않는다.
→ 누군가가 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 입력해줘야 한다.
관심사의 분리
이전 코드의 문제
- 하나의 클래스에서 **“다양한 책임”**을 갖고 있는 것이 문제
- OrderServiceImpl은 OrderService 역할도 수행하면서, DiscountPolicy까지 정하는 역할을 수행
- ex) 로미오 역할의 남자 배우(OrderServiceImpl)가 여자 배우(DiscountPolicy)까지 정하는 것과 같음
- OrderServiceImpl은 OrderService 역할도 수행하면서, DiscountPolicy까지 정하는 역할을 수행
→ 관심사를 확실히 분리!!
- 자신이 수행하는 기능과 수행을 위한 환경 세팅을 확실히 분리!!
AppConfig
Appconfig : 구현 객체를 생성하고, 연결해주는 역할을 하는 별도의 클래스
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
- AppConfig는 구현 객체를 생성
- AppConfig는 생성한 객체 인스턴스를 생성자를 통해 주입
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(long id) {
return memberRepository.findById(id);
}
}
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId); // 회원 정보 조회
int discountPrice = discountPolicy.discount(member, itemPrice); // 맴버 자체를 넘겨 해당 맴버에 대한 discountPolicy를 적용해 disCount 가격을 반환 -> OrderService에서는 discountPolicy에 대해서는 몰라도 된다. -> 단일 책임 원칙이 잘 지켜진 것!
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
- MemberServiceImpl, OrderServiceImpl이 모두 interface에만 의존 : DIP 만족
- memberRepository나 DiscountPolicy가 확장되어도 코드 수정이 없다 : OCP 만족

새로운 구조와 할인 정책 적용

- OCP, DIP 만족!
SOLID 원칙의 적용
SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.
- 기존에는 클라이언 객체가 직접 구현 객체를 생성 및 연결, 실행 하는 역할을 담당
- 구현 객체를 생성하고 연결하는 것은 AppConfig가 담당
- 클라이언트 객체는 실행하는 책임만 담당
DIP 의존 관계 역전의 원칙
추상화에 의존해야 한다. → 의존성 주입은 이 원칙을 따르는 방법 중 하나
- 클라이언트 코드가 추상화 코드에만 의존하려면, 클라이언트가 구현 객체를 생성하면 안됨
- AppConfig가 외부에서 의존성 주입(DI)을 해줌으로써, 클라이언트 코드가 추상화에만 의존할 수 있도록 함
OCP 개방 폐쇠 원칙
소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다.
- 특정 기능이 확장되어도, 다른 코드의 변경이 있어서는 안 된다.
- 다형성을 사용하고, 클라이언트가 DIP를 지킴으로써, 특정 기능이 추가 되어도 클라이언트 코드는 변경되지 않을 수 있었다.
IoC, DI, Container
IoC(Inversion of Control) 제어의 역전
개발자가 객체를 호출하는 것이 아닌, 별도의 프레임워크가 객체를 대신 호출
- 프로그램에 대한 제어 흐름에 대한 권한이 모두 AppConfig가 가지고 있다.
- 실제로 어떤 구현 객체를 생성하고, 누구에게 주입할지 등을 AppConfig가 결정하기 때문
프레임워크 vs 라이브러리
- 프레임워크 : 내가 작성한 코드에 대한 실행 등과 같은 제어를 담당
- 라이브러리 : 내가 작성한 코드를 내가 직접 실행 하는 등, 흐름 제어를 수행
DI(의존성 주입)
- 정적인 클래스 의존 관계 : 실행 전에 알 수 있는 의존 관계
- 클래스 코드 내의 import문만 보고도 알 수 있다
- 동적인 클래스 의존 관계 : 실행이 되면서 알 수 있는 의존 관계
- 어플리케이션이 실행되면, 의존성 주입을 통해서 연결되는 관계
의존성 주입을 사용하면, 정적 클래스 의존 관계를 전혀 바꾸지 않고, 동적 클래스 의존 관계를 바꿀 수 있다.
IoC Container, DI Container
- AppConfig처럼 객체를 생성해주고, 연결해주는 것을 IoC container 또는 DI container라고 한다.
- 또는 어셈블러, 오브젝트 팩토리 등이라고 불린다.
스프링으로 전환
AppConfig
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
- @Bean으로 설정한 객체를 spring bean에 등록
Main
public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member memberA = new Member(1L, "memberA", Grade.VIP);
memberService.join(memberA);
Member findMember = memberService.findMember(1L);
System.out.println("findMember = " + findMember.getName());
System.out.println("memberA = " + memberA.getName());
}
public class OrderApp {
public static void main(String[] args) {
ApplicationContext ap = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ap.getBean("memberService", MemberService.class);
OrderService orderService = ap.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 1000);
System.out.println(order);
}
}
- AnnotationConfigApplicationContext(AppConfig.class) : AppConfig에서 @bean 어노테이션이 붙은 객체를 실제 spring container에 등록
- applicationContext.getBean("memberService", "MemberService") : container에 존재하는 객체를 이름과 반환타입을 지정해서 가져올 수 있다.
- 이름은 기본적으로 AppConfig의 함수 이름으로 설정된다.
스프링 컨테이너
- ApplicationContext : 스프링 컨테이너
- 기존에는 AppConfig를 사용해 직접 객체를 생성하고 DI를 했지만, 스프링에서는 스프링 컨테이너를 통해서 사용
- 스프링 컨테이너는 @Container가 붙은 AppConfig를 설정 정보로 사용
- @Bean이라 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록
- 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
AppConfig를 통해 직접 객체를 가져오는 것이 아니라, 스프링 컨테이너에서 가져오면 더 많은 장점이 존재
'Spring(스프링) 기초' 카테고리의 다른 글
| 컴포넌트 스캔과 의존관계 자동 주입 (0) | 2023.12.08 |
|---|---|
| 스프링 싱글톤 컨테이너 (0) | 2023.12.08 |
| 스프링 컨테이너와 스프링 빈 (1) | 2023.12.08 |
| 스프링 핵심 원리 이해1 - 예제 만들기 (0) | 2023.12.08 |
| 객체 지향 설계와 스프링 (1) | 2023.12.08 |