본 포스트는 Inflearn 김영한 선생님 강의를 정리한 것 입니다!
안뇽하세용~
이번 시간에는 순수 자바로 이루어진 작은 프로젝트를 만들어보겠습니다.
이후 포스트에서 이를 스프링 프로젝트로 전환 시키며 스프링이 어떤일을 대신 해주는지 하나씩 알아보도록 하겠습니다.
먼저 스프링을 사용하진 않을 예정이지만 프로젝트 만드는게 쉬우니 Spring boot starter를 사용하겠습니다.
https://start.spring.io 요기 새창으로 띄우시고,
위 사진처럼 프로젝트를 세팅한 후 Generate로 프로젝트를 다운받아 압축을 풀어주세요.
Dependenies는 건들지 마셔요.
(spring boot에 SNAPSHOT이 붙은 경우 정식 릴리즈버젼이 아니니 가급적 사용하지 말아주세요. 이거 안붙은 아무버젼 사용하셔도 무방합니다.)
인텔리 제이를 켜시고 Open을 누른 뒤 다운받은 폴더 경로로 이동합니다.
폴더로 이동해보면 위처럼 파일들이 보이실텐데 build.gradle 파일을 열어줍니다!
Open as Project ㄱㄱ
Trust Project ㄱㄱ!
그럼 IntelliJ가 프로젝트에 필요한 라이브러리들을 전부 땡겨오기 시작합니다.
처음 프로젝트를 열면 생각보다 시간이 걸리니 잠시 폰질을 해줍니다.
아래 콘솔에서 BUILD SUCCESSFUL 이 떳다면 프로젝트가 준비된것 입니다.
2분이 넘게 걸렸네요.
자 그럼 이제부터 한명의 SI 개발자가 돼어 사장님이 따오신 프로젝트를 수행해 보도록 하겠습니다.
우선 회원가입을 할 수 있고, 회원은 일반과 VIP 두 가지 등급으로 나뉘도록 만들라고 하시네요.
단 DB는 우리가 구축할 수 도 혹은 외주를 줄 수 도 있군요... 그렇다면 나중에 변경하기 쉽도록 객체지향의 중요한 특징 다형성의 특징을 잘 살려서 만들어야겠습니다.
다음으로 회원이 상품을 주문하는데 등급에 따라 할인 정책이 달라지는군요.
VIP는 우선 1000원을 고정적으로 할인해달라고 하는데, 무조건 1000원 할인...? 딱봐도 뭔가 문제가 많아보입니다.
이건 분명 나중에 바뀔 가능성이 농후해요. 이부분도 다형성의 특징을 잘 살려서 개발해야겠습니다.
이처럼 할인 정책처럼 개발과정에서 아직 정해지지 않은 것들이 있을 수 있습니다. 그러나 마냥 결정되기를 기다릴 순 없겠죠.
그래서 우리는 먼저 인터페이스를 만들고 그 구현체를 언제든 갈아끼울 수 있도록 설계할 예정입니다.
회원 도메인 요구사항
우선 클라이언트가 사용할 회원 서비스를 만들어야합니다.
그러나 DB를 어떻게 할지 정해지지 않았기 때문에 회원 저장소라는 데이터를 건드는 계층을 별도로 만들었습니다.
이 회원 저장소는 인터페이스로 구현하여 구현체를 언제든 바꿔 낄 수 있도록 설계하였습니다.
우선 DB설계를 건들지 말고 메모리 회원 저장소라는 Java collection을 사용한 아주 간단한 구현체를 사용하겠습니다. 나중에 DB로 바꾸면 돼니까요.
이를 클래스 다이어그램으로 나타내면 위와 같습니다. (DB외부 시스템 연동은 그냥 뺏음)
우선 메모리 회원 저장소를 사용할 예정이니 서버를 켜면 연관관계가 위처럼 나오겠네요.
휴.. 대충 그림이 그려집니다.
이해 안되시면 백문이 불여일타! 그냥 코드를 다 쳐보시고 그다음에 다시한번 봐보셔용
자 이제 코드를 쳐보겠습니다.
먼저 core 패키지 하단에 member 패키지를 만들고. 그 아래 Grade객체를 enum으로 만들어 주세요.
package hello.core.member;
public enum Grade {
BASIC, VIP
}
그리고 Member 객체를 만들겠습니다.
package hello.core.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...
}
이제 회원 저장소 인터페이스를 만듭니다.
save, 그리고 findById 두 메서드를 만들어 역할을 정의해주겠습니다.
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
이제 구현체를 하나 만들어보겠습니다.
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);
}
}
MemberRepository 인터페이스를 상속받고, save, findById 메서드를 구현했습니다.
저장은 그냥 Map에다가 저장하도록 하겠습니다.
물론 ram에 잠깐 저장하니까 개발용으로만 사용하셔야해요.
그리고!
여러 사용자가 동시에 HashMap에 접근하면 동시성 이슈가 발생할 수 있습니다. 때문에 실무에서는 꼭 ConcurrentHashMap을 사용하셔야 합니다!
우리가받은 요구사항에서 회원 저장소에게 필요한 역할은 회원 가입, 그리고 회원 조회였죠??
join 그리고 findMember 두개 메서드를 정의해둡니다.
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
그리고 그 구현체를 바로 만들어주겠습니다.
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
관례상 구현체가 하나만 있을 때는 Impl 붙여서 많이 쓴다고 합니다.
이제 만든 회원 서비스가 제대로 작동하는지 테스트를 해볼 겁니다.
뭐.. 사실 당연히 잘 되겠죠 간단하기도하고 쌤이 만드신 코드니까 ...
그래도!
테스트 주도 개발!
테스트코드작성을 선택이 아닌 필수입니다.
Junit은 자바 프로젝트의 대표적인 테스트 프레임워크입니다. (스프링 부트 스타터로 만들면 바로 사용하실 수 있어요)
이녀석에 대한 설명은 흐름이 끊기니까 나중에 하도록 하겠습니다.
MemberServiceImpl에 들어가 클래스 안에 커서를 두고 shift + command + T 를 누르면 작은 창이 하나 뜨고 Create New Test... 누르시면 자동으로 테스트 클래스를 만들어줍니다.
java 폴더 말고 test폴더가 만들어져있을텐데 여기에 같은 패키지 경로로 만들어져요.
단축키를 사용하지 않고 직접 클래스를 만드셔도 괜찮습니다.
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
class MemberServiceImplTest {
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);
}
}
테스트할 MemberServiceImpl을 만들고 테스트 케이스를 작성하였습니다.
참고
Assertions가 2종류가 있습니다. core.api.Assertions를 땡겨오셔야해요.
죠기 @Test왼쪽에 초록색 실행버튼을 눌러 테스트를 진행해줍니다.
성공이네요! 😎
자이제 회원 도메인의 개발이 끝났습니다.
이 설계에서 문제점은 무었일까요?
MemberService구현체을 보면 이 객체가 MemberService 인터페이스에 의존하는 것은 맞지만, 인터페이스 뿐만 아니라 MemoryMEmberRepository라는 또다른 구현체에도 의존하고있습니다.
추상화에만 의존하라는 DIP원칙을 위반한것이지요.
또 만약 구현체를 바꿔야 할 때 우리는 new MemoryMemberRepository를 지우고 새롭게 만든 구현체를 코딩해 넣어주어야합니다.
적지만 어쨋든 기존 코드를 변경했으니 OCP또한 위반하게 되겠네요.
찜찜한 이느낌을 지울 수 가 없습니다...
우선 남은 부분을 모두 만들고 문제점과 해결방한을 찾아보도록 하겠습니다.
주문과 할인 도메인을 설계해 보도록 하겠습니다.
주문과 할인 정책
1. 회원은 상품을 주문할 수 있다.
2. 회원 등급에 따라 할인 정책을 적용할 수 있다.
3. 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
4. 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
진행 흐름을 간단히 정리해보자면
1. 주문 생성: 클라이언트가 주문 서비스 객체에게 주문을 넣습니다.
2. 회원 조회: 주문 서비스 객체는 회원 저장소에서 회원정보를 받아옵니다.
3. 할인 적용: 주문 서비스 객체는 회원 등급에 따른 할인 여부를 할인 정책에 위임합니다.
4. 주문 결과 반환: 할인 결과를 포함한 주문 결과를 반환합니다.
단 할인 정책이 아직 결정되지 않았기 때문에 우선 1000원씩 무조건 할인해주는 정액 할인 정책을 추수에 갈아끼우기 쉽도록 위 그림처럼 구현하였습니다.
글래스 다이어그램은 이렇게 될것입니다.
클래스 다이어그램을 참고해서 빠르게 빠르게 코딩해볼까요?
core 패키지아래 discount 패키지를 만들고 할인 정책 인터페이스를 만들어줍니다.
package hello.core.discount;
import hello.core.member.Member;
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
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;
}
}
}
우선 1000원을 무조건 할인해주는 FixDiscountPolicy를 만들었습니다.
회원과 상품가격을 넣으면 이 구현체가 회원 등급을 확인하고 VIP일 시 1000원을 할인한 가격을 반환하도록 만들었어요.
core 패키지 하단에 order 패키지를 만들고 Order 엔티티를 만들어줍니다.
package hello.core.order;
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;
}
...
getter
...
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
주문에 필요한 회원 정보, 상품 정보, 할인정보를 가지고있게 만들었습니다.
package hello.core.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
요구사항을 보면 주문 서비스는 주문하기 기능만 있으면 되니, createOrder 메서드 하나만 만들어줍니다.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
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);
}
}
주문이 들어오면 회원 정보를 조회하고 할인 정책을 실행하여 Order객체를 반환하도록 만들었습니다.
우선 회원 저장소, 할인 정책 모두 정해지지 않았으니 임의로 MemoryMemberRepository, FixDiscounrPolicy를 구현체로 생성하도록 코딩했습니다.
이것도 역시나 테스트 코드를 작성해야합니다.
OrderServiceImpl에서 shift + command + t를 누르시면 마찬가지로 테스트 클래스를 만들 수 있습니다.
package hello.core.order;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
class OrderServiceImplTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
@Test
void createOrder() {
//Given
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
//When
Order order = orderService.createOrder(memberId, "itemA", 10000);
//Then
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);}
}
우선 임의의 VIP 회원 memberA를 만들고 itemA라는 만원짜리 물건을 주문했습니다.
그 결과 Order 엔티티의 할인 가격이 1000원으로 잘 나오네요!
자아아아~~ 프로젝트 개발이 모두 끝났습니다.
역할과 구현을 명확히 구분하여 객체 지향 프로그램의 특징인 다형성을 야무지게 잘 활용하였습니다.
할인 정책, 회원 저장소가 변경되더라도 언제든 손쉽게 변경 가능한 좋은 프로그램이라고 할 수 있습니다.
자 그럼 다음포스트에서는 정액 할인정책을 정률 할인정책으로 바꾸면서 우리가 만든 프로그램이 정말 객체 지향적으로 훌륭한 프로그램이었는지 확인해보도록 하겠습니다.
(아니란 얘기겠죠 🥸 )
끝!