포스트

스프링 핵심 원리 이해

스프링 핵심 원리 이해

비즈니스 요구사항 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    • 할인 정책은 변경 가능성이 높다. 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)

회원 도메인

회원 도메인 개발

회원 엔티티

  • 회원 등급
1
2
3
4
public enum Grade {
  BASIC,
  VIP
}
  • 회원 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}

회원 저장소

  • 회원 저장소 인터페이스
1
2
3
4
5
6
public interface MemberRepository {

  void save(Member member);

  Member findById(Long memberId);
}
  • 메모리 회원 저장소 구현체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
  }

}

회원 서비스

  • 회원 서비스 인터페이스
1
2
3
4
5
6
public interface MemberService {

  void join(Member member);

  Member findMember(Long memberId);
}
  • 회원 서비스 구현체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
  }
}

주문과 할인 도메인 설계

주문과 할인 도메인 개발

할인 정책

  • 할인 정책 인터페이스
1
2
3
4
public interface DiscountPolicy {

  int discount(Member member, int price);
}
  • 정액 할인 정책 구현체
1
2
3
4
5
6
7
8
9
10
11
12
public class FixDiscountPolicy implements DiscountPolicy {

  private int discountFIxAmount = 1000;

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

주문 엔티티

  • 주문 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
  }

  // getter
  // setter
}

주문 서비스

  • 주문 서비스 인터페이스
1
2
3
4
public interface OrderService {

  Order createOrder(Long memberId, String itemName, int itemPrice);
}
  • 주문 서비스 구현체
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
  }
}

새로운 할인 정책 개발

서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률(%) 할인으로 변경하고 싶다고 요청이 왔다고 가정해보겠습니다.

RateDiscountPolicy 추가

1
2
3
4
5
6
7
8
9
10
11
12
public class RateDiscountPolicy implements DiscountPolicy {

  private int discountPercent = 10;

  @Override
  public int discount(Member member, int price) {
    if (member.getGrade() == Grade.VIP) {
      return price * discountPercent / 100;
    }
    return 0;
  }
}

주문 인터페이스 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class OrderServiceImpl implements OrderService {

  private final MemberRepository memberRepository;
  //        private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 변경

  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 위반: 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 줌

관심사 분리

클라이언트는 인터페이스에만 의존하고 구현체는 외부에서 주입 받도록 관심사를 분리하여 위 문제를 해결합니다.

AppConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AppConfig {

  public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
  }

  public MemoryMemberRepository memberRepository() {
    return new MemoryMemberRepository();
  }

  public OrderService orderService() {
    return new OrderServiceImpl(
      memberRepository(),
      discountPolicy()
    );
  }

  public DiscountPolicy discountPolicy() {
    //        return new FixDiscountPolicy();
    return new RateDiscountPolicy();
  }
}

클라이언트 코드 수정

  • 회원 서비스 구현체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MemberServiceImpl implements MemberService {

  private final MemberRepository memberRepository;

  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);
  }
}
  • 주문 서비스 구현체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class OrderServiceImpl implements OrderService {

  private final MemberRepository memberRepository;
  //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  //    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
  private final 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);
  }
}

IoC, DI, 그리고 컨테이너

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
  • 반면에 AppConfig가 등장한 이후로 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 AppConfig가 가져간다.
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

의존 관계 주입 DI(Dependency Injection)

  • OrderServiceImplDiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존 관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체 의존 관계 둘을 분리해서 생각해야 한다.

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존 관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존 관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.

스프링으로 전환하기

AppConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class AppConfig {

  @Bean
  public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
  }

  @Bean
  public MemoryMemberRepository memberRepository() {
    return new MemoryMemberRepository();
  }

  @Bean
  public OrderService orderService() {
    return new OrderServiceImpl(
      memberRepository(),
      discountPolicy()
    );
  }

  @Bean
  public DiscountPolicy discountPolicy() {
    //        return new FixDiscountPolicy();
    return new RateDiscountPolicy();
  }
}

스프링 DI 컨테이너에서 객체 꺼내오기

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderApp {

  private static final Logger log = LoggerFactory.getLogger(OrderApp.class);

  public static void main(String[] args) {

    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); // 어노테이션 기반 스프링 DI 컨테이너

    MemberService memberService = applicationContext.getBean("memberService", MemberService.class); // 회원 서비스 Bean
    OrderService orderService = applicationContext.getBean("orderService", OrderService.class); // 주문 서비스 Bean
  }
}

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.