스프링 핵심 원리(1)

2025. 2. 19. 03:26·Spring

 

회원 도메인 설계

회원 도메인 요구사항

- 회원 가입, 조회 가능

- 회원은 일반회원, VIP회원 두 가지 등급으로 관리

- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

 

회원 등급

package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}

 

회원 Entity

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;
    }
}

 

회원 Repository

package hello.core.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

 

메모리 Repository 구현체

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);
    }
}

* HashMap은 동시성 이슈가 발생할 수 있으니 ConcurrentHashMap 사용하면 됨

 

회원 Service

package hello.core.member;

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}

 

회원 Service 구현체

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);
    }
}

 

회원가입 테스트

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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);
    }
}

 

테스트는 정상적으로 작동한다.

그러나 위 코드의 문제

- OCP 원칙을 잘 지키는가?

- DIP를 잘 지키는가?

 

주문과 할인 도메인 설계

요구사항

- 회원은 상품을 주문할 수 있다.

- 회원 등급에 따라 할인 정책을 적용할 수 있다.

- 할인 정책은 모든 VIP는 고정적인 금액 1000원 할인 (추후 변경될 가능성 있음)

 

할인 정책 인터페이스

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

    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;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

 

주문 Entity

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 +
                '}';
    }
}

 

주문 인터페이스

package hello.core.order;

public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}

 

주문 서비스 구현체

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);
    }
}

 

주문과 할인 테스트

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);
    }
}

 

 

위 코드에선 MemberServiceImpl이 memberRepository의 구현체은 MemoryMemberService와 강하게 결합되어 있다.

따라서 OCP를 위반하고 DIP까지 어기고 있는 코드다.

 

OCP 위반

DB가 변경될 경우 MemberServiceImpl의 코드를 수정해야 한다.  -> DI로 해결 가능

 

DIP 위반

추상화의 의존해야 하는 DIP원칙을 위반중.

MemberServiceImpl이 MemberRepository라는 추상화에 의존하는게 아닌 MemoryMemberRepository에 의존 중

(구체적인 저수준 모듈에 의존하고 있다.)

MemberServiceImpl은 저장소 변경에 취약해짐 -> 생성자 주입으로 DIP 준수 가능

 

SRP (위반 가능성이 있음)

클래스는 단 하나의 책임을 가져야한다. 그러나 MemberServiceImpl이 저장소를 직접 생성하면

회원관리 로직 외에도 저장소 선택이라는 책임이 추가되는 것임.

서비스 클래스는 비즈니스 로직에 집중해야 한다. -> AppConfig로 저장소와 객체 생성 책임을 위임할 수 있음

(입문편 자바 코드로 스프링 빈 설정 강의내용)

 

LSP

위 코드는 LSP를 만족하고 있음. MemberServiceImpl은 MemberRepository의 인터페이스만 의존하고 있으므로

어떤 하위 타입이든 치환 가능함

 

ISP

위 코드는 ISP와 무관함 MemberRepository 인터페이스는 회원 저장소 역할에 맞게 잘 분리돼 있음

MemberServiceImpl은 MemberRepository의 메서드만 호출하고 과도하게 큰 인터페이스에 의존하지 않음.

 

 

다음 포스트에서 새로운 할인 정책을 적용해 위 코드를 리팩토링 해보겠다.

'Spring' 카테고리의 다른 글

스프링 컨테이너와 스프링 빈  (1) 2025.02.24
스프링 핵심 원리 (2)  (0) 2025.02.20
스프링 기초  (1) 2025.02.13
스프링 - IoC  (0) 2025.02.05
Maven 이란?  (0) 2024.08.28
'Spring' 카테고리의 다른 글
  • 스프링 컨테이너와 스프링 빈
  • 스프링 핵심 원리 (2)
  • 스프링 기초
  • 스프링 - IoC
공부처음하는사람
공부처음하는사람
  • 공부처음하는사람
    lazzzykim
    공부처음하는사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (127)
      • Kotlin (31)
      • Java (55)
      • Spring (18)
      • Algorithm (3)
      • TroubleShooting (1)
      • 내일배움캠프 프로젝트 (14)
      • Setting (2)
      • ... (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 인기 글

  • 태그

    빈 생명주기
    spring
    김영한의 실전자바
    배열
    김영한
    김영한의 실전 자바
    java
    다형성
    캡슐화
    싱글톤
    내일배움캠프
    kotlin
    @Component
    OCP
    중첩클래스
    생성자 주입
    Di
    제네릭
    언체크예외
    래퍼클래스
  • hELLO· Designed By정상우.v4.10.3
공부처음하는사람
스프링 핵심 원리(1)
상단으로

티스토리툴바