[스프링 핵심원리] 1. 객체지향 설계와 스프링
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
다음 강의를 완강하고, 복습 차 정리를 시작합니다.
먼저 프로젝트 생성을 위해 https://start.spring.io 에서 다음과 같은 항목으로 프로젝트를 생성했습니다
Project : Gradle - Groovy Project
Spring Boot : 2.3.x (3.x 버전으로 설치할 경우 java 11 이상 버전이 필요합니다)
Language: Java
Packaging : Jar
Java : 11
Dependencies : 선택하지 않음
비즈니스 요구사항과 설계
1. 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두가지 등급이 있다.
- 회원 데이터는 자체 DB 구축이 가능하고, 외부 시스템과 연동할 수 있다(아직 확정되지 않음, 변경 가능성 O)
2. 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인정책을 적용할 수 있다(등급 - ENUM)
- 할인 정책은 고정할인정책을 적용한다
- 할인 정책은 변경가능성이 있다, 안할 수도 있다.
회원 도메인 설계
1. 회원 도메인 요구사항
- 회원을 가입하고 조회할 수 있다
- 회원은 일반과 VIP 두 등급이 있다
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.
따라서 다음과 같은 다이어그램이 도출되는 것을 볼 수 있었다.
소프트웨어 공학에서 객체, 클래스 다이어그램을 공부하기 전까지 이 다이어그램이 무엇을 의미하는지 몰랐었지만,
이제는 알 수 있다.
클라이언트는 회원 서비스를 이용하고 회원 서비스는 회원가입과 회원 조회기능을 갖고 있다(Service에서 구현 필요)
회원 서비스는 이때 회원 저장소를 참조하여 두 행위를 진행하는데 이때 회원 저장소는 메모리 회원 저장소일수도, DB회원 저장소일수도
외부시스템에서 연동하는 회원 저장소일수도 있다, 따라서 회원 저장소를 Interface화하여, 구현체를 서로 달리할 필요가 있다.
회원 도메인 협력 관계와는 다르게, 회원 클래스 다이어그램에서는 실제로 어떻게 스프링 코드를 작성할 것인지 구조를 명세하고 있다.
먼저 MemberService 인터페이스의 구현체로 MemberServiceImpl 객체를 갖고
MemberRepository 인터페이스의 구현체로 MemoryMemberRepository와 DBMemberRepository를 갖는다.
MemberServiceImpl은 MemberRepository에 의존한다.
회원 도메인 개발을 위해 다음과 같은 코드를 추가하였다
// 회원 등급 구분을 위한 Enum 객체 Grade
public enum Grade {
BASIC,
VIP
}
package hello.springintroduction.member;
// 회원 엔티티로써 기본적으로 포함해야 할 정보를 내포하고 있다.
public class Member {
private Long id;
private String name;
private Grade grade;
// Constructor
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
// Getter & Setter
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;
}
}
회원 저장소 인터페이스
package hello.springintroduction.Member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
메모리 회원 저장소 구현체
package hello.springintroduction.Member;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
// 동시성 이슈에 의해 ConcurrentHashMap 사용
// ConcurrentHashMap에 대한 설명은 생략한다.
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
데이터베이스를 어떻게 사용할 것인지 확정이 되지 않았기 때문에, 다음과 같이 MemoryMemberRepository를 사용한다
회원 서비스 인터페이스
package hello.springintroduction.Member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
회원 서비스 구현체
package hello.springintroduction.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MemberServiceImpl implements MemberService{
// 다형성에 의해 MemberRepository로 MemoryMemberRepository 가져옴.
private MemberRepository memberRepository;
@Autowired // ac.getBean(MemberRepository.class)로 넣어주는 것과 비슷
// 의존관계를 자동주입함 (@Component, @AutoAppConfig 생성에 따름)
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
// 클래스 다이어그램 - 정적
// 객체 다이어그램 - 동적
}
애플리케이션 Test를 애플리케이션 로직에 집어넣는건 매우 좋지 못하다
- 따라서 JUnit을 이용하여 다음과 같이 테스트 코드를 작성하였다.
(듣기로 println도 synchronized를 내포하고 있어서, 락이 수십번 걸린다고 생각하면,,, 오우야....)
package hello.springintroduction.member;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
// 회원 서비스 테스트 코드
public class MemberServiceTest {
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
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(findMember).isEqualTo(member);
}
}
Given - When - Then 3가지 축으로 테스트 코드를 작성하는게 주된 방법이라고 한다.
isEqualTo의 경우 값 자체가 같은지 비교하고
isSameAs의 경우 객체를 비교할때 사용한다고 보면된다!
https://www.inflearn.com/questions/270335/issameas-와-isequalto-차이
isSameAs 와 isEqualTo 차이 - 인프런 | 질문 & 답변
안녕하십니까 테스트 코드 작성 중 궁금한 것이 생겨 질문드립니다.[사진]1. memberService에서 member를 조회하셨는데, 예제여서 그런 것인지, 아니면 만약에 실제 DB를 사용하여 member를 조회한다면 me
www.inflearn.com
여기서 보면 모든게 좋아보인다, (오? 일단 돌아가네?)
근데 문제는 다른 저장소로 변경할 때, 클라이언트 코드의 변경이 필요하다.
위 코드는 이미 변경된 상태지만, 그 전에는 MemberService가 MemoryMemberRepository 구현체를 그대로 의존하고 있었다
MemberRepository memberRepository = new MemoryMemberRepository();
의존관계가 인터페이스 뿐 아니라, 구현까지 모두 의존하게 되면 OCP원칙을 위배하는게 된다.
하지만 공부할때는 주문 클래스까지 구현한 뒤, 문제점과 해결방안에 대해 논의했다.
이 공부 기록은 모든 강의를 다 수강하고 복습 차원에서 작성하고 있기에 코드가 이미 다 짜여져있는 상태이다.
(다음에는 단위로 복습해야겠다)
주문과 할인 도메인 설계
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다
- 회원 등급에 따라 할인 정책을 적용할 수 있다(이전에 ENUM으로 만듬)
- 할인 정책은 고정 금액 할인으로 일단 적용한다( 추후에 변경될 수 있음! )
다음 도메인 설계 다이어그램을 보자
클라이언트는 주문 서비스에 회원 아이디, 상품명, 상품 가격을 전달해서 구입을 요청한다.
그러면 주문 서비스 객체는 회원 저장소와 할인 정책 클래스에 접근하여 이 회원 id에 대한 정보를 retrieve 하고 주문 결과를 반환한다.
역할과 구현을 다음과 같이 분리해서 구현 객체를 조립할 수 있게 설계함
-> 회원 저장소는 물론이고 할인 정책 또한 유연하게 변경할 수 있다.
다음과 같은 다이어그램을 토대로 스프링 코드를 작성하였다.
변경 가능한 DiscountPolicy의 인터페이스
package hello.springintroduction.discount;
import hello.springintroduction.Member.Member;
public interface DiscountPolicy {
int discount(Member member, int price);
}
DiscountPolicy의 구현체 중 하나인 FixDiscountPolicy 구현체
package hello.springintroduction.discount;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
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;
}
}
}
package hello.springintroduction.order;
// 주문 엔티티
public class Order {
private Long id;
private String itemName;
private int itemPrice;
private int discountPrice;
public Order(Long id, String itemName, int itemPrice, int discountPrice) {
this.id = id;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
@Override
public String toString() {
return "Order{" +
"id=" + id +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
}
주문 서비스 인터페이스 - 인터페이스 생성은 변경 가능성이 있는 항목에서 적용한다.
(업캐스팅, 다운캐스팅 등 다형성을 활용해서 유연하게 확장 가능하도록 설계)
package hello.springintroduction.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
주문 서비스 구현체
package hello.springintroduction.order;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy; // Interface에만 의존하게 됨.
// 이 상태로만 두면 NPE -> 구체 클래스 어떻게 주입?
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository=memberRepository;
this.discountPolicy=discountPolicy;
}
// DiscountPolicy만 의존하는게 아니라 FixDiscountPolicy도 의존하고 있었다!!
// DIP 위반임. 결론적으로 정책 변경으로 인해, OrderServiceImpl의 코드 또한 변경해야함.
// DiscountPolicy 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP 위반
@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);
}
}
주문 생성 요청이 들어옴과 동시에 회원정보를 조회하고, 할인 정책을 적용한 뒤 주문 객체를 생성해서 반환함.
메모리 회원 레포지토리와 고정 할인 금액 정책을 구현체로 생성함 -> 이 또한 구현체에 의존하고 있어서 좋은 코드가 아님
위 코드는 다음과 같은 좋지 못한 코드를 이미 개선한 상태임 (@Autowired, 스프링 의존관계 자동주입에서 설명)
원래 두 의존관계는 다음과 같은 형태로 기술되어 있었음 (생성자 없이 직접 주입)
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
테스트 코드를 다음과 같이 작성함
package hello.springintroduction.order;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
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 = new Order(memberId, "itemA", 10000, 1000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
다음 글로 이 코드의 문제점과 해결방법에 대해 제시하고, 코드를 수정하는 시간을 가져보겠습니다!