이번 예제에서는 spring을 사용하지 않고 순수 java로 프로그램을 만들어보자
입문 강의에서 했던 과정 그대로 반복
1. 프로젝트 생성
https://start.spring.io/
dependencies에 추가하지 않음
파일 다운 후 intellij에서 열기
2. preferences > build 관련 설정 IntelliJ로 변경하기
preferecnes 단축키 command + ,
이미 설정이 되어 있어 생략
개발자의 입장에서 요구사항에 따라 설계하자
아래 요구사항을 보면 결정되지 않은 정책이 많다.
따라서 인터페이스를 만들고 나중에 결정이 된다면 구현체로 변경하는 방식으로 설계한다.

요구사항
회원 데이터 저장소는 미확정 상태이기 때문에 가장 간단한 형태인 메모리 회원 저장소 (새로고침시 날아감)로 생성한다.
회원 도메인 협력 관계

회원 클래스 다이어그램
회원 서비스 구현체 - MemberServiceImpl
회원 저장소 - MemberRepositor, 아래 구현체 등으로 구성

객체간 참조를 나타낸 다이어그램
구현체는 동적으로 바인딩 되기 때문에, 실제 유효한 객체끼리의 참조를 나타낸
객체 다이어그램이 필요하다.

요구사항에 따라 하나씩 만들어보자.
0. member 패키지 생성
아래는 member 패키지 아래에 생성한 것들
1. 회원 등급 Grade enum 생성
Basic, VIP
2. Member 클래스 생성
3. MemberRepository 인터페이스 생성
일단 회원 저장, Id로 조회하기 기능만 만들기
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
4. MemoryMemberRepository 구현체 생성
원래는 인터페이스와 구현체를 다른 패키지에 두는게 설계상 좋음
간단한 진행을 위해 같은 member 패키지에 생성
private static Map<Long, Member> store = new HashMap<>();
이 코드에서는 HashMap을 사용했는데, 실무에서는 concurrent HashMap을 사용해야함
package hello.core.member;
import java.util.HashMap;
import java.util.Map;
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);
}
}
5. MemberService 인터페이스 생성
회원 가입, 회원 아이디로 조회 기능
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
6. MemberServiceImpl MemberService 구현체 생성
5에서 만든 인터페이스의 구현체를 만든다.
관례적으로 구현이 하나일 때는 구현체 이름에 인터페이스명+Impl 을 많이 붙인다.
저장소 객체를 만들 때, MemberRepository의 구현 객체를 선택해서 (MemoryMemberRepository) 생성한다.
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
// MemoryMemberRepository의 오버라이딩된 save가 호출된다
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원가입, 아이디로 회원 조회하는 기능 구현이 끝났다.
이제 테스트를 진행하자
지금 회원 서비스 -> 회원 저장소 기능은 다 만들었으니, 클라이언트가 회원서비스를 요청하는 부분을 만들자
1. Java로 테스트
hello.core/MemberApp 생성 (member 패키지 밖에 생성해야함)
main 메서드 생성할 때 psvm + enter 누르면 자동으로 만들어준다
memberSeervice 객체 생성 후, member join으로 회원 가입, findMember로 회원 조회 기능을 테스트 해보자
Member 생성자 인자로 id, name, Grade (enum.VIP, Basic 으로 접근)를 넘겨서 생성하고 join 메서드로 회원 가입을 한다.
이때 id 필드는 long 타입으로 선언했기 때문에 1L로 넣어줘야함 (아님 컴파일 오류)
...
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("findMember = " + findMember.getName());
System.out.println("member =" + member.getName());
}
}
결과를 조회해보면, getName으로 조회한 멤버의 이름과 회원가입시 생성한 멤버의 이름이 일치하는 것을 확인할 수 있다. (join, getName 메서드 모두 잘 작동)

2. Junit 테스트 프레임워크를 사용
main 폴더가 아닌 test 폴더 아래에 패키지를 생성
test/java/hello.core/member/MemberServiceTest 생성
give, then, when으로 나눠서 작성
Assertions org.assertj.core.api 에서 가져와야됨
위 테스트 코드와 달리, 오류의 원인이나 다른 테스트를 진행할 수 있다는 장점이 있다
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join(){
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then
Assertions.assertThat(member).isEqualTo(findMember);
}
}
지금까지 java만을 이용해서 회원 도메인 설계를 해봤는데, 이러한 설계는 문제점이 있음
아래 코드에서 MemberServiceImpl은 MemberRepository (인터페이스)와 MemoryMemberRepository (구현체) 둘다 의존하고 있음 => DIP 위반
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
이어서 java로 계속해서 주문, 할인 도메인 설계를 해보자
요구사항
주문 생성 -> 회원 조회 -> 할인 적용 -> 주문 결과 반환 순서로 진행
주문 데이터를 저장하는 것은 생략하고, 주문 결과를 반환하는 것으로 함

주문 도메인 전체
할인 정책에서 역할과 구현이 분리되어 있기 때문에 추후에 변경이 자유롭다

주문 도메인 클래스 다이어그램
위 도메인 그림처럼 인터페이스, 구현체로 표현

주문 서비스에서 저장소나, 할인정책이 변경되어도 각각에게 영향을 주지 않는다.
즉 협력 관계를 그대로 재사용할 수 있다.
0. Discount 패키지 생성
1. DiscountPolicy 인터페이스 생성
할인 정책 인터페이스 생성하기
2. FixDiscountPolicy 구현체 생성
정액 할인 정책 구현체, 등급에 따라 할인되는 금액을 리턴함
회원 등급 조회 후 VIP이면 1000 리턴
...
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;
}
}
}
0. Order 패키지 생성
1. Order 클래스 생성
memberId, itemName, itemPrice, discountPrice 필드 선언 후
command + N으로 생성자, getter setter 생성하기
금액 계산 후 리턴하는 메서드도 추가
public int calculatePrice(){
return itemPrice - discountPrice;
}
command + N 에서 toString 선택해서 객체를 출력할 때 string 형태로 출력되게
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
2. OrderService 인터페이스 생성
주문 생성시 회원Id, 상품명, 상품 가격 필요
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
3. OrderServiceImpl OrderService 구현체 생성
위에서 만든 정액 할인제를 사용하고 discountPolicy의 할인 금액을 알려주는 discount 메서드를 사용해서 할인된 가격을 Order 객체에 넣어서 리턴
할인 정책이 바뀌어도, 주문 서비스에서는 고치지 않아도 됨
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);
}
}
위 회원 도메인 테스트 처럼 java로 해봄 (생략)
그러나 Junit을 통한 자동화된 테스트를 사용하는게 좋다
마찬가지로 assertsThat을 사용하여, 생성된 주문에서의 할인 가격과
리턴 받은 주문에서의 할인 가격이 일치하는지 테스트
public class OrderServiceTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
@Test
void createOrder(){
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}