비즈니스 요구사항 설계
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
회원 도메인
회원 도메인 개발
회원 엔티티
1
2
3
4
| public enum Grade {
BASIC,
VIP
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
// getter
// setter
}
|
회원 저장소
1
2
3
4
5
6
| public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
|
회원 서비스
1
2
3
4
5
6
| public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
|
주문과 할인 도메인 설계
주문과 할인 도메인 개발
할인 정책
1
2
3
4
| public interface DiscountPolicy {
int discount(Member member, int price);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| public class FixDiscountPolicy implements DiscountPolicy {
private int discountFIxAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFIxAmount;
}
return 0;
}
}
|
주문 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
this.memberId = memberId;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
// getter
// setter
}
|
주문 서비스
1
2
3
4
| public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@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);
}
}
|
새로운 할인 정책 개발
서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률(%) 할인으로 변경하고 싶다고 요청이 왔다고 가정해보겠습니다.
RateDiscountPolicy 추가
1
2
3
4
5
6
7
8
9
10
11
12
| 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;
}
return 0;
}
}
|
주문 인터페이스 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 변경
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);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
|
문제점
- DIP 위반: 인터페이스 뿐만 아니라 구현 클래스에도 의존하고 있음
- OCP 위반: 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 줌
관심사 분리
클라이언트는 인터페이스에만 의존하고 구현체는 외부에서 주입 받도록 관심사를 분리하여 위 문제를 해결합니다.
AppConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
|
클라이언트 코드 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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 memberId) {
return memberRepository.findById(memberId);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
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);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
|
IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
- 반면에
AppConfig가 등장한 이후로 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 AppConfig가 가져간다. - 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
의존 관계 주입 DI(Dependency Injection)
OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.- 의존 관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체 의존 관계 둘을 분리해서 생각해야 한다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존 관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.- 의존 관계 주입에 초점을 맞추어 최근에는 주로
DI 컨테이너라 한다. - 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
스프링으로 전환하기
AppConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
|
스프링 DI 컨테이너에서 객체 꺼내오기
1
2
3
4
5
6
7
8
9
10
11
12
| public class OrderApp {
private static final Logger log = LoggerFactory.getLogger(OrderApp.class);
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); // 어노테이션 기반 스프링 DI 컨테이너
MemberService memberService = applicationContext.getBean("memberService", MemberService.class); // 회원 서비스 Bean
OrderService orderService = applicationContext.getBean("orderService", OrderService.class); // 주문 서비스 Bean
}
}
|
참고