본 시리즈는 우아한형제들 개발 팀장이신
김영한
님의스프링 핵심 원리 - 기본편
강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.
역할과 구현
을 나눠 개발해보기Dependency
는 비워둔다.Build and run using
과 Run tests using
의 값을 IntelliJ IDEA
로 바꾼다. (기존에는 Gradle
로 설정되어 있다.)Java
로 직접 실행하게 되는데, 이는 Gradle
을 통해 실행하는 것보다 속도가 빠르다.실습을 위해 가정한 비즈니스 요구사항은 다음과 같다.
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은
일반
과VIP
두 가지 등급으로 나뉜다.- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동될 수 있다. (미확정)
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 모든
VIP
에게 1000원을 할인해주는정액 할인제
적용- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
복잡한 요구사항이다...
이 요구사항의 난점은 현재 미확정된 부분이 있다는 점이다. 그러나 객체 지향 설계를 잘 따르면 걱정할 필요가 없다!
참고: 위에서 언급했듯이 처음에는 Spring없이 순수한 Java로 개발한다.
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은
일반
과VIP
두 가지 등급으로 나뉜다.- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동될 수 있다. (미확정)
회원 저장소
의 구현을 유연하게 교체할 수 있도록 만든다.MemberService
를 굳이 인터페이스로 만든 이유는 캡슐화를 위해서이다. 추후 구현 내용을 수정하더라도 외부에서 알 수 없도록 하기 위해.Impl
을 붙여서 구현클래스의 이름으로 삼는다.hello.core.member
패키지Grade
: 회원 등급을 enum
으로 표현
public enum Grade {
BASIC,
VIP
}
Member
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 작성하면 된다.
}
MemberRepository
: 저장소의 역할
을 표현하는 interface
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
MemoryRepository
: 메모리 저장소의 구현
을 표현하는 구현 class
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);
}
}
MemberService
public interface MemberService {
void join(Member member);
Member findMember(Long memberid);
}
MemberServiceImpl
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);
}
}
도메인 개발이 완료되었으니 이제 테스트를 해보자. hello.core
하위에 MemberApp
을 작성한다.
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());
}
}
main함수에서 테스트하는 대신 JUnit
을 사용해보자. test
폴더 하위에 hello.core.member
패키지를 생성하고 MemberServiceTest
를 작성한다.
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);
}
}
Assertions
는 assertj
의 class를 이용한다.JUnit
을 이용하면 테스트 실패 시 Error가 발생하기 때문에 훨씬 편하다.MemberServiceImpl
은 저장소의 구현체를 의존한다.
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 모든
VIP
에게 1000원을 할인해주는정액 할인제
적용- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
참고: 실제로는 주문 데이터를 DB에 저장하겠지만, 예제를 간단히 하기 위해 단순히 반환하는 것으로 했다.
주문 도메인 전체
주문 도메인 클래스 다이어그램
주문 도메인 객체 다이어그램1
주문 도메인 객체 다이어그램2
hello.core.discount
패키지DiscountPolicy
: 할인 정책의 역할
을 표현하는 interface
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
FixDiscountPolicy
: 정액 할인 정책의 구현
을 표현하는 class
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000; //1000원 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP)
return discountFixAmount;
else
return 0;
}
}
hello.core.order
패키지Order
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
// 여기에는 생성자 작성
public int calculatetPrice() {
return itemPrice - discountPrice;
}
// 여기에는 Getter, Setter 작성
// 여기에는 toString 작성 (자동 생성)
}
OrderService
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
OrderServiceImpl
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);
}
}
OrderServiceImpl
에서는 할인이 어떤 방식으로 이루어지는지 알 필요가 없다!구현을 완료했으니 테스트 코드를 작성하자!
test
폴더 하위에 hello.core.order
패키지를 생성하고 OrderServiceTest
를 작성한다.
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);
}
}
테스트가 잘 수행되는 것을 확인할 수 있다.
Alt + Insert
: Generate (생성자, Getter, Setter 등을 자동 생성)Alt + Enter
: 뭔가 빨간줄 그어졌을 때 해결psvm
치고 Enter
: 메인함수 생성sout
치고 Enter
: System.out.println
생성soutv
치고 Enter
: 각종 변수의 값을 출력하는 코드 생성Ctrl+Alt+V
: Refactoring (해당 라인의 값을 지역 변수에 할당하는 기능으로 쓸 수 있음)F2
: 에러나는 곳(빨간줄)으로 바로 이동