AtraFelis's Develop Diary

[Spring] 객체 지향 원리 적용하기 (스프링 핵심원리 - 기본편 section4) 본문

Programming/Spring

[Spring] 객체 지향 원리 적용하기 (스프링 핵심원리 - 기본편 section4)

AtraFelis 2025. 1. 11. 17:24

스프링 핵심 원리 - 기본편 강의를 수강하며 작성한 글입니다.
Section 4

 

주어진 상황

  • 기존에는 VIP 등급의 고객에게 1,000원을 고정적으로 할인해주었다.
  • 할인 정책이 고정 할인 정책에서 비율 할인 정책으로 변경되었다.
    • 비율 할인 정책 : VIP에게 10% 할인을 적용한다.
public class OrderServiceImpl implements OrderService {  
    private final MemberRepository memberRepository = new MemoryMemberRepository();  

//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();  
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();  

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

new FixDiscountPolicy(); -> new RateDiscountPolicy();

이렇게 OrderService의 구현체인 OrderServiceImpl에서 할인 정책의 구현 클래스를 FixDiscountPolicy에서 RateDiscountPolicy 로 바꾸었다.

그렇다면 DIP와 OCP를 지켰다고 할 수 있을까? 엄밀하게 말하자면 그렇지 않다.

클라이언트인 OrderServiceImpl이 인터페이스인 DiscountPolicy 뿐만 아니라 그 구현체에도 의존하고 있기 때문이다(DIP 위반). 결국, 할인 정책을 바꾸려면, 결국 클라이언트 코드인 OrderServiceImpl의 코드도 수정할 수밖에 없다 (OCP 위반).

이는 할인 정책 뿐만 아니라, 저장소인 MemberRepository 에도 해당하는 이야기이다. 지금은 개발과 테스트를 위해 간단하게 메모리 기반으로 데이터를 저장하고 있지만, 추후에 MySQL등과 같은 DBMS를 이용하게끔 변경한다면?

private final MemberRepository memberRepository = new JpaMemberRepository()

이런 식으로 바뀌어야 할 것이다. 이 역시 DIP와 OCP를 위반하게 된다.

 

해결 방법?

public class OrderServiceImpl implements OrderService {  
    private final MemberRepository memberRepository = new MemoryMemberRepository();  

    private DiscountPolicy discountPolicy;

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

이렇게 수정하면 될까?

  1. DIP : 클라이언트 코드인 OrderServiceImpldiscountPolicy 의 구현체에 의존하던 문제점이 사라졌다.
  2. OCP : 의존하던 구현체가 사라졌으므로, 할인 정책이 변경되어도 이 코드에서는 수정할게 사라졌다.

원칙은 지켜졌다. 하지만 이 상태로 실행하면 NullPointException오류가 터질 것이다. 당연히, discountPolicy에는 아무런 객체가 할당되지 않았기 때문이다.

Q. 아니, 그러면 어떻게 하는데요?
A. 누군가가 클라이언트 코드에 대신 주입을 해주면 된다.
Q. 이게 대체 무슨 소리임? 코드는 조상님이 주입해주나?

이런 문답이 오고가기 좋은 모호한 상황이 발생한다.

여기서 조상님 역할을 대신 해주는 것이 바로 AppConfig 클래스이다.

 

설정(구성) 파일 - AppConfig

AppConfig구현 객체를 생성하고, 연결하는 책임을 가진 별도의 클래스이다. 이 클래스가 위의 상황을 해결해준다.

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;  
    } 
    @Override  
    public Order createOrder(Long memberId, String itemName, int itemPrice) {...}  
}
public class AppConfig {
    public MemberService memberService() {  
        return new MemberServiceImpl(new MemoryMemberRepository());  
    }  

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

이렇게 수정을 하면 된다. 실제로 실행할 때 아래와 같이 AppConfig를 이용해 주입해주는 것이다.

아래의 코드를 보면 이해가 쉬울 것이다.

public class OrderServiceTest {  
    MemberService memberService;  
    OrderService orderService;  

    @BeforeEach  
    public void beforeEach() {  
        AppConfig appConfig = new AppConfig();  

        memberService = appConfig.memberService();  
        orderService = appConfig.orderService();
    }  

    @Test  
    void createOrder() {...}  
}

memberServiceImpl은 이제 구현체에는 관심이 없다. 즉, 어떤 할인 정책을 사용하는지, 어떤 저장소를 사용하는지에 신경 쓸 필요가 사라진 것이다.

이제 우리는 어떤 정책을 사용할지 AppConfig 클래스에서 미리 설정한 후, 생성자를 이용해 실제 실행할 때 주입하면 된다.

이러한 방식을 DI(Dependency Injection - 의존관계 주입, 의존성 주입) 이라고 하며, DI 중에서도 생성자를 이용해 주입하므로 생성자 주입이라고 한다.

 

AppConfig 리팩토링

자세히 보면 기존의 AppConfig에도 변경하여야 할 부분이 있다.

바로 memberService() 메소드와 orderService() 메소드에서 모두 new MemoryMemberRepository()를 활용해 새로운 객체를 사용하여 넘겨준다는 점이다.

아래와 같이 리팩토링하면, 하나의 객체를 공유하여 사용할 수 있게 된다.

public class AppConfig {  

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

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

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

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

MemberRepository와 더불어 DiscountPolicy 인터페이스도 같이 리팩토링 해준다. 이렇게 하면, 추후 구현체가 MemoryMemberRepository가 아닌 JpaMemberRepository로 바뀌어도,

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

이 부분만 수정을 해주면 된다.

이제 기존의 목적인 할인 정책 또한 고정 할인 정책에서 비율 할인 정책으로 변경하자. DIP, OCP의 원칙을 잘 준수했으므로, 우리가 변경해야 할 코드는 단 하나다.

AppConfigdiscountPolicy() 메소드.

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

이 부분을 요구사항에 맞추어 구현한 구현체로 부품 갈아 끼우듯 바꿔주기만 하면 성공이다.

이것이 객체 지향의 강력한 이점이다. 역할구현을 명확하게 분리하여 기존에 발생하던 귀찮은 문제점들을 해결했다.

여기까지는 스프링을 사용하지 않고 순수한 자바만을 이용해서 구현해왔다. 다음부터는 스프링을 이용하여 더욱 업그레이드를 해볼 것이다.

 


References

스프링 핵심 원리 - 기본편