Backend/Spring

스프링과 객체지향

olsohee 2023. 12. 28. 16:26

스프링의 핵심 컨셉: 객체지향

스프링은 자바 기반의 프레임워크이다. 자바의 가장 큰 특징은 객체지향 언어라는 것인데, 스프링은 객체지향 언어가 가진 강력한 특징을 살려내는 프레임워크이다. 즉 스프링의 핵심 컨셉은 객체지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.

다형성

객체지향 프로그래밍은 프로그램을 여러 객체들의 모임으로 파악하고, 각 객체들 간에 메시지를 주고받으며 협력하는 것을 강조한다. 이러한 객체지향 프로그래밍은 프로그램을 유연하고 변경에 용이하게 만든다. 프로그램이 유연하고 변경에 용이하다는 것은 무엇일까? 여기서 다형성이라는 개념이 등장한다.

 

다형성은 역할과 구현을 분리한다. 예를 들어 다음과 같이 자동차 역할과 그 구현이 나눠져있다고 가정하자.

그러면 자동차의 구현체가 K3에서 아반떼로 변경되더라도 운전자는 똑같이 운전하면 된다. 즉 클라이언트(운전자)는 인터페이스(자동차 역할)만 알고 구현체는 모르며, 구현체의 변경이 클라이언트에게 영향을 끼치지 않는다. 뿐만 아니라 구현체가 추가되는 것이 운전자에게 영향을 끼치지 않기 때문에 여러 구현체의 개발이 가능하다.

 

자바 언어에서도 다형성이 존재한다. 오버라이딩을 떠올려보자. 다음과 같이 MemberRepository의 구현체들이 save() 메소드를 오버라이딩하고 있다고 가정하자.

클라이언트인 MemberService가 MemberRepository의 save() 메소드를 호출하면, 구현체의 오버라이딩된 save() 메소드가 호출된다. 

 

다형성에 대해 정리하면 다음과 같다.

  •  
  • 다형성은 세상을 역할과 구현으로 구분한다. 이를 통해 변경에 유연하게 대처할 수 있다.
  • 클라이언트는 대상의 역할(인터페이스)만 알면 되고, 구현 대상의 내부 구조를 몰라도 된다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되거나 구현 대상이 변경되어도 영향을 받지 않는다.
  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.

다형성을 적용하더라도, OCP와 DIP를 위반하는 문제

다음 코드는 잘 설계된 것 같아 보이지만 문제가 있다.

public class MemberServiceImpl implements MemberService {

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

 

바로 좋은 객체지향의 원칙인 SOLID 중 DIP(의존관계 역전 원칙)와 OCP(개방 폐쇄 원칙)를 지키지 못한다는 것이다.

  • 클라이언트인 MemberServiceImpl이 직접 인터페이스의 구현체를 선택한다. 즉, 인터페이스 뿐만 아니라 구현체에도 의존한다. (DIP 위반)
  • 따라서 구현체를 변경하려면 MemberServiceImpl의 코드도 변경해야 한다.  (OCP 위반)

즉, 위 코드는 역할과 구현을 분리하여 다형성을 사용했지만, DIP와 OCP를 위반한다. 따라서 다형성 외에 무엇인가가 추가로 필요하다!

AppConfig의 등장

애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 갖는 별도의 설정 클래스인 AppConfig를 만들어보자.

public class AppConfig {

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

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

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

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

 

AppConfig는 애플리케이션의 동작에 필요한 구현 객체를 생성하고, 생성자를 통해서 각 객체를 연결(의존관계 주입)한다. 

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

AppConfig를 통해 MemberServiceImpl은 DIP와 OCP를 지킬 수 있게 되었다.

  • MemberServiceImpl 대신 AppConfig가 구현체를 선택한다.
  • 따라서 MemberServiceImpl은 더이상 구현체에 의존하지 않는다. MemberRepository 인터페이스만 알고 있으며, 어떤 구현체가 주입될지는 모른다. (DIP)
  • 따라서 구현체가 변경되더라도 MemberServiceImpl의 코드는 변경되지 않는다. (OCP)

MemberServiceImpl은 이제 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다. 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

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

기존에는 클라이언트(MemberServiceImpl)가 필요한 구현 객체를 스스로 생성하고 실행했다. 즉 클라이언트가 실행 외에도 객체를 생성하고 연결하는 제어 흐름을 스스로 조종했다. 반면 AppConfig가 등장한 이후에는 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 AppConfig가 담당한다. 따라서 프로그램의 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다.

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

OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 따라서 실제 어떤 구현체가 사용되는지는 모른다. 의존관계는 정적인 클래스 의존관계와 실행 시점에 결정되는 동적인 객체 인스턴스 의존관계 둘을 분리해서 생각해야 한다.

  • 정적인 클래스 의존관계: 클래스가 사용하는 import 코드만 보고 의존관계를 파악할 수 있다. 즉 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다.
  • 동적인 객체 인스턴스 의존관계: 애플리케이션 실행 시점(런타임)에 실제 생성된 객체 인스턴스가 연결되는 의존관계이다. 즉 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에게 전달해서 의존관계를 주입한다.

의존관계 주입(DI)을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다. 즉 실행시점에 의존관계가 주입된다.

IoC 컨테이너, DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하며 의존관계를 주입해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불린다. 스프링은 DI 컨테이너를 제공함으로써 다형성뿐만 아니라 DIP와 OCP를 가능하게 지원한다. 따라서 클라이언트의 코드 변경 없이 기능을 확장할 수 있고 쉽게 부품을 갈아 끼우듯이 개발할 수 있다.


Reference

  • 인프런, 스프링 핵심 원리 - 기본편, 김영한