
본 시리즈는 우아한형제들 개발 팀장이신
김영한님의스프링 핵심 원리 - 기본편강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.
역할과 구현을 나눠 개발해보기
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: 에러나는 곳(빨간줄)으로 바로 이동