@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 컨테이너를 통해 의존관계를 주입해준다.
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();
}
}
범용 인터페이스를 용도에 맞게 분리함으로서 구현체를 만들 때 필요한 기능만을 구현할 수 있게 된다.
public interface DiscountPolicy {
long apply(long amount);
}
public class FixDiscountPolicy implements DiscountPolicy {
public long apply(long amount) {
// 할인 정책의 의도와는 달리 금액을 2배로 계산
return amount * 2L;
}
}
DiscountPolicy 인터페이스를 통해 할인 정책을 적용하는 부분에서 인터페이스에서 의도한 할인과는 달리 오히려 결제 금액이 증가하는 효과를 발생할 수 있다. 이는 인터페이스를 통해 기능을 사용하는 입장에서 의도한 바가 아니므로 문제가 될 수 있다. 그러므로 인터페이스를 구현할 때에는 인터페이스에서 의도한 규약을 반드시 지켜야한다.
@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 컨테이너 등이 이에 해당한다.