OOP 회원과 주문 관리 설계

 

목차

    1. 요구사항

    2. 순수 자바 코드로 구현

     

    oop_sample/oop_sample/src at main · kyeonghooon/oop_sample

    Contribute to kyeonghooon/oop_sample development by creating an account on GitHub.

    github.com

    주요 포인트 !!

    public class AppConfig {
    
       public MemberRepository getMemberRepository() {
          return MemoryMemberRepositoryImpl.getInstance();
       }
    
       public MemberService getMemberService() {
          return new MemberServiceImpl(getMemberRepository());
       }
    
       public DiscountPolicy getDiscountPolicy() {
          // 할인 정책 --> 고정 할인
          // 변경 --> 정률 할인으로 바꾸면 된다.
          // return new FixDiscountPolicyImpl();
          return new PercentDiscountPolicyImpl();
       }
    
       public OrderService getOrderService() {
          return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
       }
    
    }

    인터페이스를 활용하여 정책이 바뀜에 따라 구현체만 바꿔서 사용할 수 있다

    만약 새로운 정책이 생긴다고 하더라도 새로운 구현체를 만들고 해당 구현체를 return 하도록 수정 하면 끝!

    3. 스프링의 빈 등록 활용 (수동 빈 등록)

     

    oop_sample/oop_sample_spring/oop at main · kyeonghooon/oop_sample

    Contribute to kyeonghooon/oop_sample development by creating an account on GitHub.

    github.com

    package com.example.oop.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import com.example.oop._domain.member.repository.MemberRepository;
    import com.example.oop._domain.member.repository.MemoryMemberRepositoryImpl;
    import com.example.oop._domain.member.service.MemberService;
    import com.example.oop._domain.member.service.MemberServiceImpl;
    import com.example.oop._domain.order.DiscountPolicy;
    import com.example.oop._domain.order.FixDiscountPolicyImpl;
    import com.example.oop._domain.order.service.OrderService;
    import com.example.oop._domain.order.service.OrderServiceImpl;
    
    @Configuration
    public class AppConfig {
    
       @Bean
       public MemberRepository memberRepository() {
          return new MemoryMemberRepositoryImpl();
       }
    
       @Bean
       public MemberService memberService() {
          return new MemberServiceImpl(memberRepository());
       }
    
       @Bean
       public DiscountPolicy discountPolicy() {
          // 할인 정책 설정 (정률 할인 사용)
          // return new PercentDiscountPolicyImpl();
          // 변경하려면 여기서 다른 구현체로 변경
          return new FixDiscountPolicyImpl();
       }
    
       @Bean
       public OrderService orderService() {
          return new OrderServiceImpl(memberRepository(), discountPolicy());
       }
    }
    package com.example.oop._domain.order.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.example.oop._domain.member.Grade;
    import com.example.oop._domain.member.Member;
    import com.example.oop._domain.member.service.MemberService;
    import com.example.oop._domain.order.Order;
    import com.example.oop._domain.order.service.OrderService;
    
    import lombok.RequiredArgsConstructor;
    
    @RestController
    @RequiredArgsConstructor
    public class TestController {
    
       private final OrderService orderService;
       private final MemberService memberService;
    
       @GetMapping("/test2")
       public String test() {
          System.out.println("ssss");
          Member member = new Member(200L, "홍길동", Grade.VIP);
          memberService.signUp(member);
          Order order = orderService.createOrder(member.getId(), "아이폰16", 3_000);
          return order.toString();
       }
    
    }
    package com.example.oop._domain.order.service;
    
    import org.springframework.stereotype.Service;
    
    import com.example.oop._domain.member.Member;
    import com.example.oop._domain.member.repository.MemberRepository;
    import com.example.oop._domain.order.DiscountPolicy;
    import com.example.oop._domain.order.Order;
    
    import lombok.RequiredArgsConstructor;
    
    @RequiredArgsConstructor
    @Service
    public class OrderServiceImpl implements OrderService {
    
       private final MemberRepository memberRepository;
       private final 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);
       }
    
    }

    4. 스프링의 빈 등록 활용 (자동 빈 등록)

    일부 예시

    package com.example.oop._domain.member.repository;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.context.annotation.Profile;
    import org.springframework.stereotype.Repository;
    
    import com.example.oop._domain.member.Grade;
    import com.example.oop._domain.member.Member;
    
    @Repository
    @Profile("dev")
    public class MemoryMemberRepositoryImpl implements MemberRepository {
    
       private Map<Long, Member> memberStore = new HashMap<>();
    
       public MemoryMemberRepositoryImpl() {
          initData();
       }//
    
       // 초기 샘플 데이터
       private void initData() {
          memberStore.put(1L, new Member(1L, "홍길동", Grade.VIP));
          memberStore.put(2L, new Member(2L, "이몽룡", Grade.VIP));
          memberStore.put(3L, new Member(3L, "성춘향", Grade.VIP));
       }
    
       @Override
       public void save(Member member) {
          memberStore.put(member.getId(), member);
       }
    
       @Override
       public Member findById(Long memberId) {
          return memberStore.get(memberId);
       }
    
    }
    • application-dev.yml 을 사용할 때 적용
    package com.example.oop._domain.order;
    
    import org.springframework.context.annotation.Primary;
    import org.springframework.stereotype.Service;
    
    import com.example.oop._domain.member.Grade;
    import com.example.oop._domain.member.Member;
    
    @Service
    @Primary
    public class FixDiscountPolicyImpl implements DiscountPolicy {
    
       private int discountFixAmount = 1500;
    
       @Override
       public int discount(Member member, int price) {
          // VIP 회원만 할인 금액 적용 된다.
          if (member.getGrade() == Grade.VIP){
             return discountFixAmount;
          }
          return 0;
       }
    
    }
    • Primary 어노테이션이 붙은 빈이 주입됨

    다양한 구현체를 사용할 수록 primary 어노테이션의 관리가 중요해질거라 생각해 따로 문서를 작성함

    ## 빈 관리 및 @Primary 사용 설명
    
    이 프로젝트에서는 여러 구현체가 존재하는 인터페이스에 대해 기본적으로 사용할 구현체를 설정하기 위해 `@Primary` 어노테이션을 사용하고 있습니다. 
    
    ### DiscountPolicy 인터페이스 구현체
    `DiscountPolicy` 인터페이스는 두 가지 구현체가 존재합니다:
    1. **PercentDiscountPolicyImpl** (정률 할인 정책)
    2. **FixDiscountPolicyImpl** (고정 할인 정책)
    
    현재 프로젝트에서는 **FixDiscountPolicyImpl**이 기본적으로 사용됩니다. 
    이를 위해 `@Primary` 어노테이션을 **FixDiscountPolicyImpl** 클래스에 적용하였습니다.

    5. 수동 등록 vs 자동 등록

    1. 수동 빈 등록 (Manual Bean Registration)

    장점:

    • 명시적 관리: 어떤 빈이 어떤 클래스에서 생성되는지 명확하게 관리할 수 있다. 빈 생성 및 의존성 주입 과정이 모두 @Configuration 클래스에 명시되므로, 전체 흐름을 쉽게 파악할 수 있다.
    • 더 큰 유연성: 특정 조건에 따라 빈을 동적으로 생성하거나, 빈의 구성 방식(예: 빈 초기화 로직)을 커스터마이징할 수 있다. 예를 들어, 특정 환경에 맞춰 다른 빈을 제공할 수 있다.
    • 빈 생성 로직 제어 가능: 빈의 생성, 초기화, 주입 등을 세밀하게 제어할 수 있으며, 필요 시 생성 시점이나 라이프사이클을 조절할 수 있다.
    • 테스트 용이성: 테스트할 때 특정 빈을 주입하거나 교체할 수 있는 유연성이 높다. 테스트에서 원하는 대로 빈을 설정하고 주입할 수 있다.

    단점:

    • 코드가 복잡해질 수 있음: 모든 빈을 직접 등록해야 하므로, 프로젝트가 커질수록 @Configuration 클래스에 빈 등록 코드가 많아질 수 있고, 관리가 복잡해질 수 있다.
    • 자동 설정의 장점 사용 불가: 스프링의 자동 구성 기능과 @ComponentScan의 이점을 충분히 활용할 수 없다. 모든 빈을 수동으로 등록해야 하므로 개발 속도가 느려질 수 있다.
    • 변경에 취약: 새로운 빈을 추가하거나 변경해야 할 때마다 빈 설정을 수정해야 하므로, 빈 관리 코드가 자주 변경될 수 있다.

    2. 자동 빈 등록 (Automatic Bean Registration)

    장점:

    • 간단하고 빠름: @Component, @Service, @Repository, @Controller 등의 어노테이션을 붙이기만 하면 빈이 자동으로 등록되므로 설정이 매우 간단하다. 추가적으로 @ComponentScan을 통해 모든 빈을 자동으로 탐색해 등록할 수 있다.
    • 스프링의 자동 설정과 통합: 스프링 부트의 자동 설정 기능을 최대한 활용할 수 있다. 즉, 스프링이 제공하는 다양한 기본 설정을 활용하여 개발 속도를 높이고, 빈 설정과 관리에 대한 수고를 덜 수 있다.
    • 간결한 코드: 빈을 별도로 등록할 필요가 없어 코드가 간결해지고 관리가 쉬워진다. 특히 작은 프로젝트나 빈의 수가 적을 때는 자동 빈 등록 방식이 매우 유리하다.

    단점:

    • 명시성이 부족: 어떤 빈이 등록되고 어디서 사용되는지 자동으로 처리되므로, 대규모 프로젝트에서는 빈의 흐름을 추적하기 어려울 수 있다. 특히 같은 인터페이스를 구현하는 여러 구현체가 있을 때 혼란이 생길 수 있다.
    • 유연성 부족: 특정 상황에 따라 빈을 다르게 생성하거나, 빈의 라이프사이클을 제어하기 어려울 수 있다. 빈의 생성 로직을 커스터마이징해야 하는 경우, 자동 빈 등록 방식은 적합하지 않다.
    • 복잡한 의존성 관리: 같은 인터페이스의 여러 구현체를 사용할 때, @Primary나 @Qualifier 같은 추가 어노테이션을 사용해 빈을 명시적으로 지정해야 하는 상황이 발생할 수 있다.

    3. 정리: 어떤 경우에 어떤 방식을 선택해야 할까?

    수동 빈 등록이 적합한 경우:

    • 복잡한 빈 관리가 필요하거나, 빈 생성 로직을 세밀하게 제어해야 하는 경우.
    • 동적으로 빈을 생성해야 하거나, 환경에 따라 다른 빈을 주입해야 하는 경우.
    • 명시적인 빈 관리가 필요하고, 테스트 환경에서 특정 빈을 쉽게 교체하거나 설정해야 할 때.

    자동 빈 등록이 적합한 경우:

    • 작고 간단한 프로젝트에서, 빈의 수가 많지 않으며 자동으로 생성해도 큰 문제가 없을 때.
    • 빠르고 간편한 개발이 필요할 때, 즉 빈 설정에 대한 수고를 덜고, 스프링의 자동 설정 기능을 최대한 활용하고 싶을 때.
    • 빈의 생성 로직이 단순하고, 특별한 빈 커스터마이징이 필요하지 않은 경우.

    결론:

    • 자동 빈 등록은 간단하고 빈의 수가 많지 않으며, 특별한 커스터마이징 없이 자동으로 등록될 때 유리하다.
    • 수동 빈 등록은 복잡한 프로젝트에서 빈을 제어하고 관리할 필요가 있을 때 적합히다.