위의 요구사항을 스프링 프레임워크를 사용하지 않고 순수 java 코드로 먼저 구현해본다.
회원도메인은 위와 같이 설계한다. 클라이언트는 회원 서비스를 이용하고 회원 서비스는 회원가입, 회원조회 기능을 수행할 때 회원 저장소에서 데이터를 읽고 저장한다.
회원서비스와 데이터를 저장하는 실제 구현은 변경될 가능성이 있으므로 인터페이스 기반으로 설계한다.
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;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
id, 이름, 등급을 가지는 간단한 회원 클래스를 만든다.
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
데이터를 어떤 방식으로 어느 db에 저장해야할지 정해지지 않은 상황에서 임시로 회원을 저장할 저장소를 만들고 변경이 가능하도록 하기위해 인터페이스를 만든다.
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);
}
}
임시로 어플리케이션을 실행하는동안 회원을 메모리에 저장하여 관리하도록 하는 구현 클래스를 만든다.
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
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);
}
}
회원 서비스는 회원가입, 회원 조회를 하기위해 MemberRepository를 클래스 내부에 포함하고있다. 회원서비스 구현체는 회원 저장소에 의존한다.
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
멤버어플리케이션이 실행되면서 멤버 서비스 객체를 생성한다. 이때 멤버서비스 구현체를 객체로 생성한다. 회원가입 및 회원 조회 기능을 수행한다.
주문 도메인 전체
주문 도메인 클래스 다이어그램
public interface DiscountPolicy {
int discount(Member member, int price);
}
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
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 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;
}
public int calculatePrice() {
return itemPrice - discountPrice;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
}
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
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);
}
}
public class OrderApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
System.out.println("order.cal = " + order.calculatePrice());
}
}
위와 같이 회원서비스와 주문할인서비스를 순수 자바 코드로 구현하였다.
이러한 상황에서 할인 정책을 변경해야하는 상황이 생겼다고 가정해보자. 할인 정책의 변경을 고려하여 인터페이스 기반으로 설계하였고 정률 할인 정책에 대한 구현체도 만들어 놓았다. 다음과 같이 주문 서비스에서 할인 정책의 구현체만 변경하면 된다.
주문서비스 구현체
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy new FixDiscountPolicy();
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은 DiscountPolicy를 포함하므로 DiscountPolicy 인터페이스에 의존한다. 하지만 인터페이스의 구현 클래스를 직접 객체로 생성하고 있으므로 RateDiscountPolicy에도 의존하는 소스가 된다.
변경에는 닫혀있고 확장에는 열려있어야한다. 정률 할인 정책 기능을 확장하기 위해 변경하면 OrderServiceImpl(클라이언트) 코드에 영향을 준다.
인터페이스에만 의존하도록 변경한다.
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;
}
//...
}
주문 서비스에서 회원 저장소와 할인 정책에 대한 구현 객체를 직접 생성하지 않도록 변경한다. 생성자를 통해 필드를 초기화한다.
그럼 생성자를 통해 구현 객체는 누가 주입해주는 것일까?
어플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
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);
}
}
객체 인스턴스의 관계는 다음과 같다.
appConfig는 memberServiceImpl 객체를 생성할 때 memoryMemberRepository 객체를 생성한 뒤 memberServiceImpl 객체를 생성하면서 생성자로 전달한다.
MemberApp 에서의 사용
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
}
}
OrderApp 에서의 사용
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
System.out.println("order.cal = " + order.calculatePrice());
}
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
기능을 확장, 변경하여도 구성 영역에만 영향을 받고 사용 영역은 영향을 받지 않는다.
SRP (단일 책임 원칙)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 클라이언트 객체는 실행하는 책임만 담당한다.
- 구성 클래스는 구현 객체를 생성하고 연결하는 책임을 담당한다.
DIP (의존관계 역전 원칙)
- 추상(인터페이스)에만 의존한다.
- AppConfig가 객체 인스턴스를 대신 생성하여 클라이언트 코드에 의존관계를 주입한다.
OCP (개방-폐쇄 원칙)
- 확장에는 열려있으나 변경에는 닫혀있어야 한다.
- 애플리케이션을 사용 영역과 구성 영역으로 나눈다.
- AppConfig가 의존관계를 변경하여 주입하므로 클라이언트 코드는 변경하지 않아도 된다.
- 소프트웨어 요소를 새롭게 확장하여도 사용영역의 변경은 닫혀있다.
클래스 다이어그램
객체 다이어그램
IoC 컨테이너, DI 컨테이너
- AppConfig와 같이 객체를 생성하고 관리하며 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
System.out.println("order.cal = " + order.calculatePrice());
}
}