Spring

[Spring] Spring 핵심 원리 : 순수한 Java 애플리케이션 - 관심사 분리

yjk490 2023. 6. 23. 16:24

객체지향 설계 원칙 위반

지금까지 주문과 할인 도메인을 개발했다. 각 객체들을 인터페이스와 구현 클래스로 나누어 다형성을 적용하며 객체지향 설계 원칙을 준수한 것처럼 보인다. 그러나 DIP, OCP, SRP를 위반했다. OrderServiceImpl를 보며 확인해 보자.

public class OrderServiceImpl implements OrderService {
    // 인터페이스 뿐만 아니라 구현 클래스에도 의존하고 있음.
    // 저장소나 할인 정책을 변경하고 싶다면 아래 코드를 수정해야 함. 
    private final OrderRepository orderRepository = new MemoryOrderRepository();
    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);

        Order order = new Order(memberId, itemName, itemPrice, discountPrice);
        orderRepository.saveOrder(order);

        return order;
    }

    @Override
    public Order findOrder(Long orderId) {
        return orderRepository.findById(orderId);
    }
}

멤버변수를 선언하는 코드를 보면 참조변수는 인터페이스 타입으로 선언했지만 실제로 대입되는 객체(의 주소값)는 구현 객체이다. 즉, 인터페이스뿐만 아니라 구현 클래스에도 의존하기 때문에 DIP를 위반했다. 구현 클래스에 의존하므로 만약 다른 저장소나 할인 정책을 사용하고 싶다면 해당 객체를 생성하는 코드를 수정해야 할 것이다. 즉, 기능을 추가할 때 클라이언트 코드도 수정해야 하므로 OCP를 위반했다. 또한 OrderServiceImpl은 주문 관련 비즈니스 로직을 처리한다. 그런데 로직 수행에 필요한 객체들도 생성하므로 SRP를 위반했다.

객체지향 설계 원칙을 준수하려면 멤버변수를 선언하는 코드는 아래와 같이 수정해야 할 것이다. 그러면 인터페이스에만 의존하므로 저장소나 할인 정책을 변경해도 OrderServiceImpl은 수정하지 않아도 된다. 물론 의존하는 객체를 직접 생성하지도 않는다.

    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

그런데 이 상태로 애플리케이션을 실행하면 NullPointerException이 발생할 것이다. 이 문제를 어떻게 해결할 수 있을까? 

관심사 분리와 의존성 주입

구현 객체들을 생성하고 연결하는 책임을 갖는 클래스를 정의해서 관심사를 분리하면 된다. 그리고 클라이언트가 사용하는 객체를 이 클래스를 통해 주입한다. 이처럼 클라이언트가 의존하는 객체를 외부에서 주입해 주는 것을 의존성 주입이라고 한다.

구현 객체들을 생성하고 연결하는 책임을 갖는 AppConfig를 다음과 같이 정의하고, OrderServiceImpl이 의존하는 객체는 생성자를 통해 주입받도록 각 클래스를 수정한다. MemberServiceImpl도 마찬가지다.

public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryOrderRepository(),
                                    new MemoryMemberRepository(),
                                    new FixDiscountPolicy());
    }
}

 

 // OrderServiceImpl.java
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

// MemberServiceImpl.java
    private final MemberRepository memberRepository;

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

이제 XXXServiceImpl은 외부에서 어떤 구현 객체가 주입될지 알 수 없다. 단지 스스로 실행만 하면 된다. XXXServiceImpl 이 의존하는 객체는 AppConfig가 결정한다. 만약 할인 정책을 변경하고 싶다면 AppConfig의 orderService() 메서드에서 할인 정책 구현체를 생성하는 코드를 수정하기만 하면 된다.

그러나 아직 문제가 있다. MemberRepository 인터페이스는 OrderServiceImpl, MemberServiceImpl 두 군데에서 사용되기 때문에 MemoryMemberRepository가 아닌 다른 구현체를 사용하고 싶다면 AppConfig의 두 메서드 모두 수정해야 한다. 이러한 중복 수정을 피하기 위해 AppConfig를 다음과 같이 수정한다.

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

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

    public OrderRepository orderRepository() {
        return new MemoryOrderRepository();
    }

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

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

이제 기존 구현체가 아닌 다른 구현체를 사용하고 싶다면 AppConfig에서 단 한줄의 코드만 수정하면 된다. 이로써 앞서 위반했던 객체지향 설계 원칙을 모두 준수할 수 있다. 이처럼 객체를 생성하고 의존관계를 연결해 주는 것을 DI컨테이너라고 한다.

실행

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

        Member savedMember = memberService.signUp(new Member(1L, "memberA", Grade.VIP));
        Order savedOrder = orderService.createOrder(savedMember.getMemberId(), "3series", 50000);
        Order foundOrder = orderService.findOrder(savedOrder.getOrderId());

        System.out.println("savedOrder = " + savedOrder.toString());
        System.out.println("foundOrder = " + foundOrder.toString());
    }
}
/* 실행 결과
savedOrder = Order{orderId=1, memberId=1, itemName='3series', itemPrice=50000, discountPrice=1000}
foundOrder = Order{orderId=1, memberId=1, itemName='3series', itemPrice=50000, discountPrice=1000}
*/

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

        Member member = new Member(1L, "memberA", Grade.VIP);
        Member savedMember = memberService.signUp(member);
        Member foundMember = memberService.findMember(member.getMemberId());

        System.out.println("findMember = " + foundMember.toString());
        System.out.println("savedMember = " + savedMember.toString());
    }
}
/* 실행 결과
findMember = Member{memberId=1, name='memberA', grade=VIP}
savedMember = Member{memberId=1, name='memberA', grade=VIP}
*/