Intro
본 카테고리는 Inflearn 김영한 강사님의 스프링 핵심 원리 강의를 수강하며 이해하고 학습한 내용을 정리한 내용으로 구성되어 있다.
본 포스팅에서는 순수 자바코드와, 이 코드에 객체지향 설계원칙을 적용해나가는 과정을 담았다.
비즈니스 요구사항과 설계
회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
주문과 할인 정책
회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
Code Overview
member
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;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
멤버에서는 가입한 회원의 id, 이름, 등급(VIP
/BASIC
)라는 멤버변수를 갖는다.
MemberRepository
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
Member
의 정보를 저장할 MemberRepository interface
이다.
추상화된 interface
로, 구현체인 Member
가 interface
인 MemberRepository
를 의존하게끔 설계 할 것이다.
또한, 하단의 MemberMemoryRepository
로 MemberRepository
를 구현한다.
MemberMemoryRepository
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);
}
}
MemberMemoryRepository
는 별도의 database를 갖지 않고 memory에 저장될 휘발성 저장소이다.
추후에 database에 연결할 때 구현 class
만 바꿔끼면 된다.
MemberService
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
Member
가 사용할 MemberService
이다.
마찬가지로 추상화된 interface
로, 하단의 MemberServiceImpl
에서 구현되어 있다.
- MemberServiceImpl
package hello.core.member;
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);
}
}
MemberService
의 추상 메서드들을 override하여 구현했다.
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;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
주문 양식을 갖고 있는 Order class
이다.
OrderService
package hello.core.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
추상화된 interface
형태의 OrderService
이다.
하단의 OrderServiceImpl
에서 이를 구현할 것이다
OrderServiceImpl
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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);
}
}
추상화된 interface
인 MemberRepository
와 DiscountPolicy
에 의존하는 OrderServiceImpl
이다.
기존 코드의 문제점
바로 위에서 설명한 OrderServiceImpl
을 다시 보자.
우선 OrderServiceImpl
(구현체)가 추상화된 interface
에만 의존하는 것이 아니다.MemberMemoryRepository
, FixDiscountPolicy
라는 구현체에도 의존하고 있기에 DIP를 위반한다.
그리고 새로운 할인 정책이 도입되었을 때, 구현체인 ServiceImpl
의 코드까지 수정해야 한다.
그러므로 OCP까지 위반하게 된다.
마지막으로 구현 객체를 생성하고, 연결하고, 또 서비스(회원가입 등)을 실행하는 다양한 책임을 가지게 된다.
이는 SRP(단일책임원칙, 한 클래스는 한 가지 책임만 가진다.)도 위반하게 된다.
이를 해결하기 위해서는 OrderServiceImpl
이란 클래스에서 interface
에만 의존하도록 코드를 수정해야 한다.
다음과 같이 코드를 수정하고, 이 멤버변수를 초기화 할 때 쓰일 생성자도 추가한다.
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@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);
}
}
...
이렇게 설계를 하며 DIP, OCP, SRP를 적용할 수 있었다.
OrderServiceImpl
뿐만 아니라 MemberServiceImpl
에도 동일한 문제가 있기에 마찬가지로 문제를 해결하고, AppConfig
에서 구현 객체를 생성해줄 것이다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
새로운 문제점의 발생
이렇게 의존 관계를 해결하여, 구현체에 의존하는 문제(DIP)를 해결하고, 또 개방폐쇄원칙인 OCP원칙까지 해결할 수 있었다.
하지만, 멤버변수들이 이렇게 인터페이스에만 의존하게 되면 코드를 실행할 수 없다.
결국, 어딘가에서 멤버변수에 구현체를 초기화해주어야 한다.
(MemberServiceImpl
의 경우 memberRepository
)
(OrderServiceImpl
의 경우 memberRepository
, discountPolicy
)
여기서 AppConfig
라는 새로운 클래스를 만들어, 구현 객체를 생성하며 연결해주는 책임을 갖게 할 수 있다.
이렇게 어플리케이션의 실행, 설정 등 다양한 책임들을 분리하는 것을 SoC(Seperation of Concerns), 즉 관심사의 분리라고 한다.
AppConfig
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
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());
}
private static DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
이렇게 AppConfig
은 어플리케이션 실행에 있어 구현 객체를 생성해주고, 이 구현 객체들을 구현 클래스의 생성자를 통해 연결해준다.
설계를 변경하게 되면서 구현체인 MemberServiceImpl
, OrderServiceImpl
은 더이상 MemberMemoryRepository
와 FixDiscountPolicy
를 의존하지 않는다.
*Impl 클래스의 입장에서 보면 의존관계를 외부에서 주입하는 형태를 가진다.
그래서 이를 DI (Dependency Injection), 즉 의존성 주입 (의존관계 주입)이라 한다.
우리는 생성자를 이용하여 주입하였으므로 이를 생성자 주입이라 한다.
- 참고 : 의존주입의 종류
- 생성자 주입(Constructor Injection)
- 필드 주입(Field Injection)
- 수정자 주입(Setter Injection)
testcode를 통해 잘 적용되었는지 확인해보자.
Test Code (JUnit)
Test
- RateDiscountPolicyTest
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.\*;
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
// given
Member member = new Member(1L, "memberVIP", Grade.VIP);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
// given
Member member = new Member(1L, "memberBASIC", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(0);
}
}
회원 등급에 따른 할인에 대한 testcode이다.
실행 결과 (O)
-
MemberServiceTest
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@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);
}
}
새로운 Member
가 회원가입을 하고, 이를 다시 찾아내어 제대로 회원가입이 되었는지 확인하는 testcode
실행 결과 (O)
-
OrderServiceTest
package hello.core.order;
import hello.core.AppConfig;
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.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@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);
}
}
주문 생성이 제대로 되고, member
의 grade
에 따른 할인 정책이 잘 적용되었는지를 확인하기 위한 testcode
- 실행 결과 (O)
감사합니다.
'Dev > Spring' 카테고리의 다른 글
[스프링 핵심 원리] @Qualifier, @Primary와 Bean 우선순위 (0) | 2023.09.26 |
---|---|
[스프링 핵심 원리] ComponentScan, 의존 관계 자동주입 (0) | 2023.09.26 |
[스프링 핵심 원리] Singleton Pattern of Spring Container (0) | 2023.09.24 |
[스프링 핵심 원리] Spring Container & Bean (0) | 2023.09.22 |
[스프링 핵심 원리] Spring 적용 (0) | 2023.09.20 |