본문 바로가기
Back-end/Spring

8_객체지향의 원리 적용

by 디지털 전산일지 2025. 4. 25.

Summary

  • [4-2] 서비스 구현체(클라이언트)에서 인터페이스 = new 구현 객체() 형태로 사용하면 인터페이스와 구현 객체 둘 다에 의존하게 되므로 OCP, DIP 원칙에 위배된다.
  • [4-3~4-7] 이를 해결하기 위해 객체를 관리할 때 AppConfig를 따로 두어 주입(DI) 하는 방식으로 관리하면 OCP, DIP 원칙을 만족시킬 수 있다. (순수 JAVA 사용)
  • [4-8~4-9] Spring으로 스프링 컨테이너와 스프링 빈을 통해 관리할 수 있다. (Spring 사용)
  • 의문점 : 스프링 빈으로 객체를 관리하니까 코드만 더 길어지는 것 같다. 어떤 장점이 있을까? -> 다음 챕터에서 의문 해소

4-2. OCP, DIP 원칙을 지키지 않은 사례

 

위 이미지와 같이 현재 주문 시스템을 개발하고 있다.

  • 회원 관리는 DB로도 할 수 있고, 메모리로 관리할 수도 있는데 아직 정확하게 어떤걸로 할지 미정이다.
  • 할인 정책도 정률 정책과 정액 정책 중 어떤걸로 할지 미정이다.

 

이 상황에서 주문 서비스의 구현체 코드를 아래와 같이 작성하였다.

public class OrderServiceImpl implements OrderService {
     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);
         return new Order(memberId, itemName, itemPrice, discountPrice);
     }
}

 

위 코드는 다형성을 만족했다. 하지만, 객체지향의 OCP와 DIP 원칙을 지키지 않았다.

왜냐하면, OCP 원칙의 경우 확장에는 열려있고 수정에는 닫혀 있어야 하는데, 만약에 할인 정책을 정액(FixDiscountPolicy 구현체)가 아니라 정률(RateDiscountPolicy 구현체)를 사용해야 한다면? OrderServiceImpl에서 아래와 같이 코드를 수정해야 하기 때문이다.

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

 

또한, DIP 원칙의 경우 클라이언트(OrderServiceImpl)가 인터페이스나 추상 클래스만을 의존해야 한다. 그런데, 인터페이스(MemberRepository, DiscountPolicy) 뿐만이 아니라 인터페이스의 구현체(MemoryMemberRepository, FixDiscount) 도 의존하고 있다. 즉, 아래 코드는 DIP 원칙을 어긴 것이다.

// discountPolicy(인터페이스)와 FixDiscountPolicy(구현체)에 의존한다.
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

 

OCP 원칙과 DIP 원칙을 지키지 않으면 나중에 유지보수를 할 때 불편함이 존재한다.

가령, 나중에 기획자가 "저희는 메모리를 사용하여 저장하는 방식이 아니라, DB 방식으로 사용하기로 했어요." 라고 한다면 클라이언트(구현체를 사용하는 OrderServiceImpl) 에서 코드를 찾아 일일이 수정해주어야 한다. 아래처럼 말이다.

     // private final MemberRepository memberRepository = new MemoryMemberRepository(); // Memory
     private final MemberRepository memberRepository = new DBMemberRepository(); // DB

 

프로젝트 규모가 복잡해진다면 유지보수에 시간(비용)이 발생할 것이다.

 

4-3. OCP, DIP 원칙을 만족하는 방법

그럼 어떻게 하면 OCP, DIP 원칙을 만족할 수 있을까?

아래와 같이 수정하면 인터페이스만 의존하므로 DIP 원칙을 만족한다. 그러나, memberRepository 객체에는 따로 지정한 것이 없으므로 null이 담겨 null pointer Exception이 발생한다.

// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;

MemberRepository.save(); // null pointer exception 발생

누군가가 클라이언트(OrderServiceImpl) 에 MemberRepository나 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다. -> AppConfig 로 해결

 

 

AppConfig는 구현 객체를 생성하고, 연결하는 역할을 하는 클래스이다.

public class AppConfig {
     public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
     }
     public OrderService orderService() {
         return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
     }
}

 

아래 코드와 같이 생성자를 통해 객체를 주입한다.

public class OrderServiceImpl implements OrderService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    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) {
        Member member = memberRepository.findById(memberId); // 회원 정보 조회
        int discountPrice = discountPolicy.discount(member, itemPrice); // 할인 정책

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

이렇게 하면, OrderServiceImpl는 인터페이스인 MemberRepository와 DiscountPolicy 에만 의존하므로 DIP를 만족한다. 또한, 할인 정책이나 데이터 저장 방식이 변경되어도 OrderServiceImpl 에 있는 코드는 변경할 필요가 없으므로 OCP를 만족한다.

 

또 OrderServiceImpl 에서는 비즈니스 로직(기능)에만 초점을 맞추고, AppConfig는 구현 객체를 관리하는 역할을 담당한다. (즉, OrderServiceImpl은 이제부터 의존관계에 대한 고민은 외부(AppConfig)에 맡기고 기능에만 집중하면 된다.)

 

이렇게 설계하면 하나의 클래스가 하나의 책임만 지는 SRP(단일 책임 원칙)도 지킬 수 있다.

 

4-4~4-5. AppConfig 리팩토링

현재 AppConfig 에는 중복도 있고 역할에 따른 구현이 잘 보이지 않는다.

  • new MemoryMemberRepository() 부분이 중복된다.
  • MemberRepository의 역할/MemberService의 역할/ OrderService의 역할/ DiscountPolicy의 역할이 한 눈에 보이지 않는다.
public class AppConfig {
     public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
     }
     public OrderService orderService() {
         return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
     }
}

 

중복되는 곳에 [Ctrl + Alt + M] 단축키를 눌러서 리팩토링을 진행한다.

public class AppConfig {
	
    // 유저 정보는 메모리(MemoryMemberRepository)로 관리한다.
    private static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
	
    // memberService 구현은 MemberServiceImpl을 사용한다.
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

	// 할인 정책은 FixDiscountPolicy을 사용한다.
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
    
    // OrderService 구현은 OrderServiceImpl을 사용한다.
    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), discountPolicy());
    }
}

 

위 코드처럼 수정하면, 각각의 역할이 잘 드러나고 중복도 제거되었다.

나중에 유저 정보를 DB로 관리하고 싶다면, AppConfig에서 아래와 같이 한 줄만 수정하면 된다.

 

4-8. IoC, DI, DI 컨테이너

 

https://hyeonstone.tistory.com/entry/4IoC-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-DI-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88

 

4_IoC, 스프링 컨테이너, DI, 스프링 빈

참고아래 도서를 일부 각색하였습니다. (이해를 위해 저만의 용어를 사용하고 내용을 일부 추가)스프링 부트 3 백엔드 개발자 되기 - 자바편SummaryIoC : 객체의 생성과 관리를 개발자가 하는 것이

hyeonstone.tistory.com

 

IoC(Inversion of Control, 제어의 역전)

기존에는 객체의 생성과 관리를 클라이언트가 (OrderServiceImpl)가 해야 했다. 그래서 개발자가 필요할 때마다 new()을 통해 구현 객체를 지정한다. 즉, 클라이언트가 프로그램의 제어 흐름을 스스로 조종했다. (아래 코드 참고) 

public class OrderServiceImpl implements OrderService {
     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);
         return new Order(memberId, itemName, itemPrice, discountPrice);
     }
}

 

그런데, AppConfig 등장 이후에는 클라이언트는 자신의 로직만 실행하고 AppConfig가 프로그램의 제어 흐름을 조종한다. (아래 코드 참고)

public class OrderServiceImpl implements OrderService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    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) {
        Member member = memberRepository.findById(memberId); // 회원 정보 조회
        int discountPrice = discountPolicy.discount(member, itemPrice); // 할인 정책

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
public class AppConfig {
	
    // 유저 정보는 메모리(MemoryMemberRepository)로 관리한다.
    private static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
	
    // memberService 구현은 MemberServiceImpl을 사용한다.
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

	// 할인 정책은 FixDiscountPolicy을 사용한다.
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
    
    // OrderService 구현은 OrderServiceImpl을 사용한다.
    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), discountPolicy());
    }
}

 

이렇게 프로그램의 제어 흐름을 클라이언트가 직접 제어하는 것이 아니라 외부(AppConfig)에서 관리하는 것을 IoC 라고 한다.

 즉, 기존에는 객체를 생성하고 관리하는 것을 클라이언트에서 하는데, 이 것을 외부에서 관리해주는 것을 제어가 역전되었다고 하고 IoC라고 부른다.

 

DI(Dependency Injection, 의존관계 주입)

애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다. (위 AppConfig, OrderServiceImpl 코드 참고)

 

 

DI 컨테이너

AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 DI 컨테이너라고 한다.

스프링에서 컨테이너는 객체를 관리하는 통이다.

 

 

4-9. AppConfig 스프링으로 전환하기 (스프링 빈 적용)

4-2 에서는 아래 코드처럼 요구사항이 바뀌면 구현 부분을 수정해야 한다. 이는 OCP와 DIP를 만족하지 않는다.

public class OrderServiceImpl implements OrderService {
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}


4-3~4-7에서 순수 JAVA로 AppConfig을 작성하여 객체를 AppConfig에서 관리하도록 하였다. OCP와 DIP를 만족시켰다.

public class AppConfig {
     public OrderService orderService() {
         return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
     }
}
public class OrderServiceImpl implements OrderService {
    private final DiscountPolicy discountPolicy;
    
    // 생성자를 통해 주입(DI)
    public OrderServiceImpl(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

 

이제 Spring을 사용하여 객체를 관리하도록 해보자.

1) AppConfig 클래스에 Configuration 어노테이션을 붙이고, 스프링 빈으로 둘 것을 Bean 어노테이션으로 붙여놓는다.

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), discountPolicy());
    }
}

 

2) 사용할 때는 스프링 컨테이너 ApplicationContext 객체를 만들고, getBean으로 접근해서 사용할 수 있다.

public class MemberApp {
    public static void main(String[] args) {
        // DIP, OCP 위반
        //        MemberService memberService = new MemberServiceImpl();

        // 순수 JAVA AppConfig 사용
//        AppConfig appconfig = new AppConfig();
//        MemberService memberService = appconfig.memberService();

        // Spring Bean 사용
        // applicationContext : 스프링 컨테이너
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); // AppConfig 클래스에서 Bean으로 등록한 것을 applicationContext 컨테이너에 넣는다.
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);// memberService 객체를 찾는다. 그 타입은 MemberService 타입이다.

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());

    }
}

 

4-9의 의문점

스프링 빈으로 객체를 관리하니까 코드만 더 길어지는 것 같은데.. 어떤 장점이 있을까?

 

참고 자료

이해를 위해 인프런_김영한님의 '스프링 핵심 원리 - 기본편'의 '섹션 4. 스프링 핵심 원리 이해2 - 객체 지향의 원리 적용'을 참고하여 쉬운 예시로 작성하였습니다.