FreeHand

[스프링 Core] 스프링 핵심 원리 - 기본편1 본문

Web/Spring

[스프링 Core] 스프링 핵심 원리 - 기본편1

Jinn 2023. 9. 14. 01:37

인프런 김영한님 [스프링 핵심 원리 - 기본편] 강의 정리

 

 

 

순수 자바로 작성

회원 도메인

회원 도메인은 저장소가 확실히 결정되지 않은 상태이다.

따라서 MemberRepository라는 인터페이스를 의존하는 관계로 작성한다.

 

  • Repository
public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}

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);
    }
}
  • Service
public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

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

 

할인과 주문 도메인

할인 역시 정확한 할인 정책이 정해지지 않은 상황이다.

따라서 주문 서비스는 인터페이스인 DiscountPolicy를 의존하도록 설계한다.

 

public interface DiscountPolicy {
    // @return 할인 대상 금액
    int discount(Member member, int price);
}

public class FixDiscountPolicy implements DiscountPolicy{

    private int discountFixAmount = 1000; // 1000원 할인

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}
public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice);
}

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 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);
    }
}
public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder() {
        // given
        long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);
        
        // when
        Order order = orderService.createOrder(memberId, "itemA", 10000);
        
        // then
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

 

 

객체지향 원리 적용

위의 코드를 보면 할인 정책으로 FixDiscountPolicy를 사용하고 있다.

만약 할인 정책을 RateDiscountPolicy로 변경하려면 OrderServiceImpl을 수정해야 한다.

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    // new FixDiscountPolicy -> new RateDiscountPolicy로 변경 필요
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        // 생략
    }
}

DIP와 OCP를 위반하고 있기 때문에 이러한 코드 수정이 필요한 것이다.

DIP를 지키기 위해 인터페이스를 의존하도록 설계했지만, 코드를 보면 인터페이스와 구현체 모두 의존하고 있다.

private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

따라서 다음과 같은 변경이 발생하고, 결국 할인 정책을 변경하기 위해 OrderServiceImpl의 코드도 수정해야 하는 것이다.

즉 OCP가 지켜지지 않는다.

 

이런 문제를 해결하기 위해 코드를 다음과 같이 수정한다.

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    // 생략
}
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    // 생략
}

두 서비스 클래스에서 구현체는 직접 생성하지 않고 생성자를 통해 주입 받도록 수정했다.

이를 통해 인터페이스만 의존하여 DIP를 지킬 수 있게 되었다.

 

public class AppConfig {

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

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

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

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

구현체를 직접 생성하고 주입하는 역할은 AppConfig라는 클래스에서 하도록 작성한다.

 

결과적으로 할인 정책을 RateDiscountPolicy로 바꾸려면 OrderServiceImpl은 변경이 없고, AppConfig의 discountPolicy 메서드만 수정하면 된다.

 

 

스프링 적용

이제 위 코드에 스프링을 적용한다.

@Configuration
public class AppConfig {

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

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

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

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

우선 AppConfig 클래스에 @Configuration 어노테이션을 추가하고 메서드에는 @Bean 어노테이션을 추가한다.

 

public class MemberApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member1 = new Member(1L, "member1", Grade.VIP);
        memberService.join(member1);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member1.getName());
        System.out.println("find member = " + findMember.getName());
    }
}
public class OrderApp {
    public static void main(String[] args) {

//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(1L, "itemA", 20000);

        System.out.println("order = " + order);
    }
}

기존에 AppConfig 객체가 생성되는 부분은 이제 필요없다.

대신 ApplicationContext 객체를 생성한다. 이 객체가 스프링 컨테이너라고 생각하면 된다.

 

@Configuration이 붙은 클래스에서 @Bean이 붙은 모든 메서드를 실행하여 반환되는 객체를 스프링 컨테이너에 Bean 객체로 등록한다. 이때 특별히 명시하지 않으면 빈 객체의 이름은 메서드 이름과 같다.

따라서 getBean("빈객체명", 타입.class)로 해당 빈 객체를 호출할 수 있다.