의존관계 역전 법칙

  • Dependency Inversion Principle
  • 외부 모듈을 의존함에 있어 구현체에 의존하는 것이 아닌, 인터페이스에 의존해야 한다.

의존관계를 구현체에 의존하게 되면 변경이 어려워진다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
//    private final FixDiscountPolicy fixDiscountPolicy;    - 정책 변경으로 인한 코드 변경
    private final PercentDiscountPolicy percentDiscountPolicy;

    public void addHistory (long amount) {
//        amount = fixDiscountPolicy.apply(amount);         - 정책 변경으로 인한 코드 변경
        amount = percentDiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public class FixDiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}


public class PercentDiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}
  • 구현체에 의존시 다른 구현체로 바꿔야 될 때 기존 구현체에 해당하는 부분을 모두 바꿔줘야한다. 기존 구현체에 의존하는 부분이 많을 수록 변경 작업이 어려워진다.

의존 관계는 구현체가 아닌 인터페이스에 의존해야한다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    // 실제 적용될 정책은 외부에서 결정된다.
    private final DiscountPolicy discountPolicy;

    public void addHistory (long amount) {
        amount = discountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}


@Component
public class PercentDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}
  • 의존관계를 인터페이스에 의존하고 실제 사용할 구현체를 외부에서 결정하도록 위임함으로서 사용될 구현체에 상관없이 의존하는 쪽에서는 코드를 변경하지 않아도 된다.
  • 스프링의 경우 런타임 시점에 구현체를 결정하여 생성하고 스프링 IoC 컨테이너를 통해 의존관계를 주입해준다.

인터페이스 분리 원칙

  • Interface Segregation Principle
  • 여러 용도의 범용 인터페이스는 작은 단위의 여러 인터페이스로 분리해야한다.

 

범용 인터페이스는 불필요한 기능의 구현을 강요한다

public interface CommonService {

    // 사용자 목록 조회
    List<Account> getUsers();

    // 게시물 목록 조회
    List<Board> getBoards();
}


@Service
@RequiredArgsConstructor
public class UserServiceImpl implements CommonService {

    private final UserRepository userRepository;
    private final BoardRepository boardRepository;

    @Override
    public List<Account> getUsers() {
        // 유저 목록 조죄
        return userRepository.getList();
    }

    @Override
    public List<Board> getBoards() {
        // 게시글 목록 조회 - 해당 클래스에서 사용하지 않는 불필요한 기능
        return boardRepository.getList();
    }
}

  • 인터페이스에 여러 용도의 기능이 뭉쳐있다면 단일 용도를 위해서 구현체를 만들 때 불필요한 기능의 구현까지 강요받게 된다.

 

범용 인터페이스를 분리하라

public interface BoardService {

    // 게시물 목록 조회
    List<Board> getBoards();
}


@Service
@RequiredArgsConstructor
public class BoardServiceImpl implements BoardService {

    private final BoardRepository boardRepository;

    @Override
    public List<Board> getBoards() {
        // 게시글 목록 조회 - 해당 클래스에서 사용하지 않는 불필요한 기능
        return boardRepository.getList();
    }
}


public interface UserService {
    // 사용자 목록 조회
    List<Account> getUsers();
}


@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    public List<Account> getUsers() {
        // 유저 목록 조죄
        return userRepository.getList();
    }
}

  • 범용 인터페이스를 용도에 맞게 분리함으로서 구현체를 만들 때 필요한 기능만을 구현할 수 있게 된다.

 

다음글

리스코프 치환 원칙

  • Liskov Substitution Principle
  • 하위 타입의 인스턴스는 부모 타입의 인스턴스로 치환할 수 있어야한다.
  • 하위 타입은 부모 타입의 인터페이스의 규약을 지켜야한다.
public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        // 할인 정책의 의도와는 달리 금액을 2배로 계산
        return amount * 2L;
    }
}

  • DiscountPolicy 인터페이스를 통해 할인 정책을 적용하는 부분에서 인터페이스에서 의도한 할인과는 달리 오히려 결제 금액이 증가하는 효과를 발생할 수 있다. 이는 인터페이스를 통해 기능을 사용하는 입장에서 의도한 바가 아니므로 문제가 될 수 있다. 그러므로 인터페이스를 구현할 때에는 인터페이스에서 의도한 규약을 반드시 지켜야한다.

 

다음글

개방-패쇄 원칙

  • Open / Close Principle
  • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

구현 클래스를 직접 사용하는 것은 변경이 번거롭다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
//    private final FixDiscountPolicy fixDiscountPolicy;    - 할인 정책 변경으로 인한 미사용
    private final PercentDiscountPolicy percentDiscountPolicy;

    public void addHistory (long amount) {
//        amount = fixDiscountPolicy.apply(amount);      - 할인 정책 변경으로 인한 미사용
        
        // 할인 정책 변경 적용
        amount = percentDiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public class FixDiscountPolicy {

    public long apply(long amount) {
        return amount - 5000L;
    }

}

public class PercentDiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}

  • 위의 코드의 PaymentService 에서 사용하는 할인 정책을 변경하려면 기존 할인 정책을 사용한 코드를 변경해야한다. 또한, 같은 할인 정책을 사용하는 모듈이 여러개 존재할 경우 모두 찾아서 변경해야한다. 그뿐만 아니라 할인정책이 변경되었는데 결제를 담당하는 PaymentService 의 코드를 변경해야된다는 점에서 단일 책임 원칙에도 위배되고 있다.

 

인터페이스를 활용하자

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final DiscountPolicy DiscountPolicy;

    public void addHistory (long amount) {
        amount = DiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy{

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}

public class PercentDiscountPolicy implements DiscountPolicy{

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}

  • 기존에 할인 정책의 구현체를 직접 의존하던 것을 인터페이스를 통해 의존하도록 변경하였다. 이를 통해 새로운 정책이 필요할 때에는 인터페이스를 구현한 새로운 구현체를 만들어 확장할 수 있고 이를 적용할 때에도 기존에 코드를 변경하지 않고 적용할 수 있다. 즉, 기존 코드를 유지한채로 기능을 확장할 수 있다. 다만, 외부에서 구현체를 생성하고 연관관계를 맺어주는 별도의 모듈이 필요하며 스프링의 IoC 컨테이너 등이 이에 해당한다.

 

다음글

단일 책임 원칙

  • Single Responsibility Principle
  • 하나의 클래스나 모듈은 하나의 책임만을 가져야 한다.
    • 책임을 나누는 중요한 기준은 변경으로 '해당 코드를 변경하려는 이유는 무엇인가?' 에 대한 질문에 단 하나의 이유만 존재해야함

 

책임을 분리하지 않을 경우의 문제점

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;

    public void addHistory (long amount, AddProduct addProduct) {
        // 할인 정책 적용
        if (amount >= 100000) {
            amount *= 0.8;
        }
        else if (amount >= 50000) {
            amount *= 0.9;
        }

        // 추가 상품 구매 여부
        if (AddProduct.TUMBLER.equals(addProduct)) {
            amount += 30000L;
        } else if (AddProduct.BAG.equals(addProduct)) {
            amount += 10000L;
        }

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public enum AddProduct {
    BAG, TUMBLER
}

  • 위에 코드에서 addHistory 는 내부에 할인 정책에 따른 금액 계산과 추가 상품 구매시 추가 금액 계산까지 모두 처리하고 있다. 이는 요구사항이 추가 및 변경되었을 때 다음과 같은 문제를 발생시킬 수 있다.
    • 요구사항이 추가되었을 경우 해당 코드처럼 작성한다면 하나의 메서드에 작성하는 코드가 길어짐
    • 여러 작업이 작성되어있는 코드 중 일부를 변경해야 할 때 코드가 길어질 경우 변경 부분을 찾기 어려워질 수 있고 개발자의 실수로 잘못된 부분을 변경할 수 있음

 

책임을 분리하라!

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final DiscountPolicy discountPolicy;

    public void addHistory (long amount, AddProduct addProduct) {
        // 할인 정책 적용
        amount = discountPolicy.apply(amount);

        // 추가 상품 구매 여부
        if (addProduct != null) {
            amount += addProduct.getValue();
        }

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public enum AddProduct {
    BAG(30000), TUMBLER(10000);

    private long value;

    AddProduct(long value) {
        this.value = value;
    }

    public long getValue() {
        return value;
    }
}


@Component
public class DiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }
}

  • 이전 코드를 수정하여 할인 정책 계산과 추가 상품 구매 금액 부분을 외부로 분리하여 위임하도록 변경하였다. 이를 통해 다음과 같은 장점을 얻을 수 있다.
    • 변경사항이 발생할 경우 해당 변경사항을 책임지는 모듈만 변경하면 됨. 즉, 변경 포인트가 명확해짐
    • 변경사항 외의 다른 부분을 건드리는 실수를 방지할 수 있다.
    • 분리한 모듈을 다른 부분에서 재활용할 수 있다.

 

다음글

+ Recent posts