개발쿠키

[Spring]관심사의 분리 본문

개발/spring boot

[Spring]관심사의 분리

쿠키와개발 2023. 8. 7. 21:18

관심사의 분리란?

영한님의 설명에 의하면 쉽게 말하자면 자신이 맡은 역할과 책임에만 집중하며 객체를 생성하고 연결하는 역활과 실행하는 역할이 명확히 분리되어있는 것을 의미한다.. 

 

애플리케이션을 공연이라 했을 때 인터페이스들을 배역이라고 가정하자. 이때 실제 배역에 맞는 배우를 선택하는 것을 누가할 것인가라고 봤을 때 각 배역을 맡은 배우들이 선택하는 것이 아니라 제 3자가 선택해주는 것이 맞다.

 

만약 로미오의 배역을 A라는 배우가 했을 때 줄리엣이라는 배역의 배우를 B 배우가 해야한다 라고 정해버리는 순간 A의 책임은 공연을 해야하는 책임에서 줄리엣의 배역까지 정해버리는 책임까지 가지게 된다. 

 

다시 말해 OOP의 원칙 중 단일 책임의 원칙(SRP)을 위반하게 된다. 이를 위해 나온 것이 관심사의 분리이다. 

 

배우는 본인의 역할인 공연을 수행하는 것에 집중해야 한다. 또한 공연을 할 때 상대 배우가 누구인지와 상관없이 공연이 수행되어야 한다. 그럼 이를 위반한 코드는 어떤 코드인가? 아래 예시를 보자 참고로 MemberRepository, DiscountPolicy는 인터페이스이다.

public class OrderServiceImpl implements OrderService {

    private final MemerRepository memerRepository = new MemoryMeberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}

주문 서비스 구현체는 MemberRepository와 DiscountPolicy에 즉 추상화 된 인터페이스에 의존하고 있다. 하지만 자세히 보면 인터페이스에 의존하는 것처럼 보이지만 실제로는 new 연산자를 통해 각 인터페이스의 구현체에 의존한다. 이를 위의 예시로 보면 A 배우가 B 배우, C 배우를 직접 정하고 있는 상태인 것이다. 

 

이때 관심사를 분리하여 보자고 하면 MemberRepository, DiscountPolicy의 구현체(배우)는 알 필요가 없고 오직 어떤 인터페이스(배역)과 상호작용하는지만 알고 해당 구현체는 역할(공연)에만 집중하는 것이다. 

 

그럼 어떻게 관심사의 분리를 적용할 수 있는가라고 하면 간단하다 각 배역의 배우를 다른 3자(강의에서는 공연 기획자라고 표현하셨다)가 지정하면 된다. 


적용

공연을 시작하기 전에 배우를 정하듯이 애플리케이션도 똑같이 적용해주면 된다. 애플리케이션이 실제로 동작하기 전에 외부 설정을 통해 어떤 구현체들이 적용될지를 적용하면 된다. 바로 아래와 같이 말이다.

public class AppConfig {
	public MemerRepository memerRepository() {
        return new MemoryMeberRepository();
    }

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
    
    public OrderServiceImpl orderService() {
        return new OrderServiceImpl(memerRepository(), discountPolicy());
    }
}

이렇게 설정했을 경우 OrderServiceImpl에서 어떤 구현체를 사용할지를 AppConfig가 결정하여 주입하게 되며 기존의 OrderServiceImpl객체는 아래 코드와 같이 변경된다. 

public class OrderServiceImpl implements OrderService {

    private final MemerRepository memerRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemerRepository memerRepository, DiscountPolicy discountPolicy) {
        this.memerRepository = memerRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order orderAdd(Long memberId, String itemName, int itemPrice) {
        Member m = memerRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(m, itemPrice);

        return new Order(1L,itemName, m, itemPrice, discountPrice);
    }
}

이때 OrderServiceImpl는 생성자를 통해 의존 관계를 주입 받을 수 있으며 이를 생성자 주입이라고 한다. 그리고 이처럼 해당 객체가 아닌 외부에서 의존관계를 결정하여 주입 하는것을 DI(Dependency Injection) 의존성 주입 또는 의존관계 주입이라고 한다. 이렇게 했을 경우 OrderServiceImpl는 어떤 구현체가 주입될지는 알 수 없으며 오직 실행에만 집중하면 된다.

 

그럼 이제 코드에서 어떤 장점을 가지게 되는지 보면 주문 서비스는 기존에 MemberRepository, DiscountPolicy를 직접 생성하는 책임을 가지고 있었다. 

 

만약 MemberRepository가 Memory가 아닌 Mysql DB로 저장소로 바뀌거나 DiscountPolicy가 기획상 정책이 바뀌면서 할인 방식이 정해진 금액이 아닌 상품의 10프로를 할인해주는 정책으로 바뀐다면 Fix 구현체가 아닌 Rate 구현체로 변경이 되면서 코드가 수정된다. 

 

이렇게 코드의 수정이 이루어질 경우 OCP(open-closed principle)를 위반하게 되며 결국은 유연하지 못한 코드가 되는 것이다. 하지만 우리는 의존관계를 외부에서 설정하도록 변경하면서 더 이상 코드를 수정하지 않아도 기존의 코드들이 동작할 수 있으며 변경과 확장에도 유연해질 수 있는 장점을 가지게 된다. 

 

SLP, DIP, OCP

그렇다면 AppConfig를 통해 의존관계를 설정하면 우리는 객체지향원칙 중 어떤 원칙들이 적용되었는가 하면 SLP, DIP, OCP 이 세가지가 적용된 것을 알 수 있다. 

 

SLP - 한 클래스는 하나의 책임만을 가져야 한다.

우리는 구현객체를 생성하고 연결하는 책임을 AppConfig에게 넘기면서 한가지 책임 즉 주문 서비스라는 책임 하나만을 가지도록 만들었다. 이렇게 한 클래스는 한가지의 책임만을 가져야 보다 효율적인 개발이 가능하다. 

 

DIP - 추상화에 의존해야하며 구체화에 의존하면 안된다.

변경된 코드에서 주문 서비스는 구현객체가 아닌 오직 인터페이스에만 의존하고 있다. 이 말이 추상화에 의존하는 것을 뜻하며 DIP 원칙이 적용된 것이라고 볼 수 있다. 

 

OCP - 확장에는 열려있고 변경에는 닫혀 있어야 한다.

소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있는 것을 의미한다.

주문 서비스에서 할인 정책이 Fix정책에서 Rate로 변경되어 새로운 요소가 추가되었지만 주문 서비스의 코드는 수정이 일어나지 않았다. 이러한 부분이 OCP를 적용한 것이며 다형성과 DIP를 잘 적용하면 OCP 적용이 더욱 쉬워진다.

 

 

마무리

OOP를 공부하면서 가장 어렵게 느껴졌던 SOLID 원칙이 어느정도 가닥이 잡혔다.


*해당 글은 김영한님의 스프링 핵심 원리 기본편을 바탕으로 작성되었습니다.