Home 회원,주문,할인 설계와 개발 예제 (1) - 순수 자바
Post
Cancel

회원,주문,할인 설계와 개발 예제 (1) - 순수 자바

‘우선 스프링의 편리함을 알기 위해 순수 자바코드를 통한 예제를 작성’

start.spring.io 에서 시작하지만 우선 Dependencies에 아무것도 추가하지 않음. (Spring Web, Thymeleaf 등등)

비즈니스 요구사항 & 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.(미정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용(추후 변경 가능성)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)

미확정이 된 부분들을 잘 파악하고 인터페이스를 만들고 구현체를 언제든 갈아끼울 수 있도록 설계해보자.

회원 도메인 설계

회원 데이터는 자체 DB or 외부 시스템과의 연동 중 미정이기 때문에 회원 데이터에 접근하는 계층을 따로 만들어준다.

회원 저장소 인터페이스를 만들고 메모리 회원저장소, DB 회원저장소, 외부시스템 연동 회원저장소를 구현체로 만들면 된다.

기획자도 볼 수 있는 수준의 협력관계

위의 협력관계를 바탕으로 개발자가 만든 클래스 다이어그램. MemoryMemberRepo 같은 경우 언제 얘를 불러올지 모름. DbMember을 부를지도 모르기 때문에 아래의 다이어그램을 작성하게 됨.

클라이언트는 MemberServiceImpl에 접근하게 되고 그 MemberServiceImpl은 멤버 리포지토리에 접근해 데이터를 얻는다.

회원 도메인 개발

Alt + Insert로 Getter, Setter, 생성자 자동 삽입 가능.

Member/Member Member/MemberRepository Member/Grade -> 등급 Member/MemoryMemberRepository Member/MemberService Member/MemberServiceImpl

구현.

코드 참고 - ppt파일

회원 도메인 실행 & 테스트

core/MemberApp

main 함수를 따로 만들어주었음.

이렇게 실행해서 확인하는 방법은 비효율적임.

테스트 코드를 작성하는게 바람직 함.

given - 무엇이 주어졌을 때

when - 어떠한 상황에서

then - given과 when의 결과가 맞는지.

이러한 코드는 DIP 잘 지키지 못함.

1
private final MemberRepository memberRepository = new MemoryMemberRepository();

직접 선언함으로써 구현체에 의존하고 있음.

주문과 할인 도메인 설계

  1. 클라이언트는 주문을 생성한다.
  2. 할인을 위해서는 회원등급이 필요하다. 그래서 주문서비스는 회원 저장소에서 회원을 조회해야 한다.
  3. 주문서비스는 회원 등급에 따라 할인 여부를 할인 정책에 위임한다.
  4. 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.

** 실제로는 주문 데이터를 DB에 저장하겠지만 일단 예제이므로 단순히 주문 결과를 반환한다.

  • 할인
1
2
3
4
5
public interface DiscountPolicy {  
  
	//@return이 할인 대상 금액  
	int discount(Member member, int price);  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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;  
		}  
	}  
}
  • 주문
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Order {  
  
	private Long memberId;  
	private String itemName;  
	private int itemPrice;  
	private int discountPrice;

	//생성자 및 getter, setter..

	public int calculatePrice(){
		return itemPrice - discountPrice;
	}
}
1
2
3
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);  
	}  
}

주문 할인 도메인 실행 & 테스트

새로운 할인 정책 개발

서비스 오픈 직전에 할인 정책을 현재의 고정 금액 할인이 아닌 주문 금액당 할인하는 정률 % 할인으로 변경하고 싶다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//RateDicountPolicy 추가.

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;  
		}  
	}  
}
  • 테스트

Assertions.assertThat을 위와같이 assertThat으로 바꾸고 싶다면 Assertions에 Alt+Enter를 입력하여 static import하여 사용 가능하다.

새로운 할인 정책의 적용

할인 정책을 변경하려면 클라이언트인 ‘OrderServiceImpl’ 코드를 고쳐야 함.

1
2
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
-> private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

문제점

  • 우리는 역할과 구현을 충실하게 분리했다.
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다.
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다? -> X

DIP : 주문서비스 클라이언트는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같다?

인터페이스 뿐만 아니라 구현 클래스에도 의존하고 있다.

OCP : 변경하지 않고 확장할 수 있어야 하는데?

OrderServiceImpl 코드를 변경해야 한다.

변경이 작던 크던 변경이 발생하면 위반이다.

1
private DiscountPolicy discountPolicy;

로 코드를 바꿀 수 있다면 인터페이스에만 의존하게 된다.

이렇게 되려면 누군가가 클라이언트인 ‘OrderServiceImpl’ 에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

관심사의 분리

애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배역이라고 하자. 실제 배역을 맡는 배우를 누가 고르나?

예를 들어 로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지, 줄리엣 역할을 누가 할지 배우들이 정하는 것이 아니다.

현재 우리 코드는 실제 배역을 맡는 배우를 배우 스스로가 정하고 있다. 배우는 배역을 수행하는 것에만 집중하면 된다.

공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 가지는 공연기획자가 필요하다.

AppConfig의 등장

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, ‘구현객체’를 생성하고 ‘연결’하는 책임을 가지는 별도의 설정 클래스를 가진다.

1
2
3
4
5
6
7
8
9
10
11
//core/AppConfig
public class AppConfig {  

	public MemberService memberService(){  
		return new MemberServiceImpl(new MemoryMemberRepository());  
	}  
  
	public OrderService orderService(){  
		return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());  
	}  
}

각각의 MemberServiceImpl 클래스와 OrderServiceImpl에는 원래

1
2
3
4
5
6
//MemberServiceImpl
private final MemberRepository memberRepository = new MemoryMemberRepository();

//OrderServiceImpl
private final MemberRepository memberRepository = new MemoryMemberRepository();  
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

와 같이 직접 구현체를 지정해주었었다.

1
2
3
4
5
6
7
8
9
//OrderServiceImpl

private final MemberRepository memberRepository;  
private final DiscountPolicy discountPolicy;  
  
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {  
		this.memberRepository = memberRepository;  
		this.discountPolicy = discountPolicy;  
	}

이와 같이 AppConfig에서 구현체를 지정해주고 그 지정해주는 것을 받기위해 생성자를 통하면 Impl 클래스는 인터페이스에만 의존하게 돼 DIP를 준수하게 된다.

즉, AppConfig가 구현객체를 모두 생성해주는 것이고 그것을 생성자를 통해 주입(연결)하게 되는 것이다.

Impl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부 (AppConfig) 에서만 결정된다.

Impl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 이를 DI(Dependency Injection) 의존관계 주입 이라고 표현한다.

AppConfig의 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//MemberApp

public class MemberApp {  
  
public static void main(String[] args){  
		AppConfig appConfig = new AppConfig();  
		MemberService memberService = appConfig.memberService();  
		// 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());  
	}  
}

appconfig를 통해 MemberServiceImpl을 불러오게 된다. appConfig에서 메모리멤버가 아닌 jdbc 멤버로 수정한다면 결과는 바뀌게 될 것이다.

이렇게 수정되면 테스트 같은 경우에도 아래와 같이 바꾸어주어야 한다.

Impl 클래스는 이로써 기능을 실행하는 책임만 지면 된다.

AppConfig 리팩토링

현재의 AppConfig는 중복이 있고 역할에 따른 구현이 잘 안보인다.

AppConfig는 한눈에 딱 보여야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//AppConfig

public class AppConfig {  
  
	public MemberService memberService(){  
		return new MemberServiceImpl(memberRepository());  
	}  
  
	private static MemoryMemberRepository memberRepository() {  
		return new MemoryMemberRepository();  
	}  
  
  
	public OrderService orderService(){  
		return new OrderServiceImpl(memberRepository(), discountPolicy());  
	}  
  
	public DiscountPolicy discountPolicy(){  
		return new FixDiscountPolicy();  
	}  
}

new MemoryMemberRepository() -> memberRepository()로 바뀜으로써 중복을 없애고 한 번만 수정하면 되도록 바뀌었다.

ctrl + alt + M 단축키 활용하면 이렇게 나누기 쉽다.

IOC, DI, 컨테이너

IOC (Inversion of Control) 제어의 역전

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.

반면 AppConfig가 등장한 이후 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. OrderServiceImpl을 예시로 들면 필요한 인터페이스들을 호출할 때 그 인터페이스가 어떤 구현객체를 가지고 있는지 알지 못한다. (알면 안된다.)

이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.

** 프레임워크 vs 라이브러리

프레임워크는 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크.

내가 작성한 코드가 직접 제어 흐름을 담당한다면 라이브러리.

DI (Dependency Injection) 의존관계 주입

OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다.

의존관계는 정적인 클래스 의존관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존관계 둘을 분리해서 생각해야 한다.

  • 정적인 클래스 의존관계

클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 코드만 보고 분석할 수 있다.

OrderServiceImpl은 MemberRepo, DiscountRepo에 의존하는데 이러한 의존관계만으로는 어떤 구현객체가 들어갈 지 알 수 없다.

  • 동적인 객체 의존관계

애플리케이션 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 ‘의존관계 주입’이라고 한다.

IoC 컨테이너, DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 연결해주는 것.

최근에는 의존관계 주입에 초점을 맞추어DI 컨테이너라고 부른다.

This post is licensed under CC BY 4.0 by the author.