Web/Spring

[Spring] 스프링의 탄생목적 - SOLID, DI 컨테이너

이번 포스팅은 앞으로 스프링을 제대로 학습하기에 앞서, 스프링이 어떤 목적을 가지고 탄생했는지 알아보려고 합니다.

 

 

 

스프링을 왜 만들었을까

스프링은 좋은 객체지향 어플리케이션을 개발할 수 있도록 도와주는 프레임워크 입니다.

개발자들은 객체지향 프로그래밍을 통해 유지보수에 좋은 코드를 작성하려고 노력했습니다.

그래서 좋은 객체지향을 위한 5가지 원칙 (SOLID) 가 탄생했습니다.

 

 

 

객체지향프로그래밍의 5가지 원칙

SRP (Single Responsibility Principle)

단일 책임 원칙

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

‘한 클래스가 하나의 책임만 가져야 한다’ 라는 말만 보면 의미가 모호할 수 있습니다.

그래서 SRP의 중요한 기준은 변경이 있을 때 파급 효과가 적도록 만드는 것입니다.

 

예를들어 Domain과 View의 책임을 나누어 UI 변경시에 view 의 클래스만 수정할 수 있도록 하는 것이죠.

만약에 domain 안에 view의 로직이 존재한다면 view를 수정해야할 때 domain 클래스의 수정이 불가피해 집니다.

 

그래서 변경이 있을 때 파급 효과가 적도록 한 클래스는 하나의 책임만 가지게 하는 것이 SRP 입니다.

 

 

 

OCP (Open / Closed Priciple)

개방-폐쇄 원칙

  • 확장에 열려있으나 변경에는 닫혀있어야 한다.

어떻게 확장에는 열려있고 변경에 닫혀있는 코드를 작성할 수 있을까요?

객체끼리 협력할 때 인터페이스와만 협력하면 됩니다.

public class MemberService {
		private MemberRepository memberRepository = new MemoryMemberRepository();
}

public class MemberService {
		// private MemberRepository memberRepository = new MemoryMemberRepository();
		private MemberRepository memberRepository = new JdbcMemberRepository();
}

MemberService가 MemberRepository인터페이스를 가지고 있다면, 새로운 기능을 추가할 때 MemberRepository를 구현한 새로운 구현체를 할당하면 됩니다.

 

하지면 여기서 문제점이 있습니다.

결국에는 개발자가 직접 구현체를 갈아 끼워주어야 합니다.

OCP를 지키기 위해 인터페이스를 가지고 있도록 했는데, 결국에는 구현체를 바꾸기 위해서 클라이언트 코드를 수정해야 합니다.

 

 

 

LSP (Liskov Substitution Principle)

리스코프 치환 원칙

  • 하위클래스는 인터페이스의 규약을 지켜서 작성하도록 해야한다.
  • 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

리스코프 치환 원칙은 컴파일타임에 컴파일러가 잡아줄 수 있는 원칙은 아닙니다.

 

예를들어 어떤 인터페이스에 moveForward() 라는 (앞으로 전진하라) 메서드가 존재하고 하위 클래스에서 moveForward()를 구현한다고 생각해보겠습니다.

그런데 하위클래스에서 moveForward()를 옆으로 가거나 뒤로가도록 구현하면 이것은 리스코프 치환 원칙에 위배되는 행동이겠죠.

개발자가 직접 지켜야 하는 원칙입니다.

 

 

 

ISP (Interface Segregation Principle)

인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 인터페이스를 분리하면 더 명확해지고 대체 가능성이 높아진다.

 

자동차 인터페이스가 있다고 생각해보면, 운전하는 인터페이스와 정비하는 인터페이스로 분리하는 것이 더 좋습니다.

 

자동차 인터페이스 하나만 존재할 때

  • 운전하는 부분의 메서드를 수정하면 정비하는 인터페이스도 영향을 받습니다.

자동차 인터페이스를 (운전하는 인터페이스, 정비하는 인터페이스)로 분리할 때

  • 운전하는 인터페이스를 수정해도 정비하는 인터페이스에 영향을 주지 않습니다.

 

 

DIP (Dependency Inversion Principle)

의존관계 역전 원칙

  • 개발자는 추상화(인터페이스)에 의존해야하지, 구체화(구현체)에 의존하면 안된다.

구현체가 아닌 인터페이스에 의존하라는 말입니다.

public class MemberService {
		private MemberRepository memberRepository = new MemoryMemberRepository();
}

public class MemberService {
		// private MemberRepository memberRepository = new MemoryMemberRepository();
		private MemberRepository memberRepository = new JdbcMemberRepository();
}

MemberService는 인터페이스에 의존하는 동시에 구현체에도 의존하고 있습니다.

DIP를 위배하고 있습니다.

 

 

DIP를 지키는 코드를 작성하기 위해선 어떻게 해야 할까요?

외부(다른 클래스)에서 MemberService에 의존성을 주입해주어야 합니다.

public class MemberService implements MemberService{

    private final MemberRepository memberRepository;

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

같은 방식으로 OCP를 위배했던 코드도 해결할 수 있습니다.

 

그렇다면, 앞으로도 외부(사용하는 곳)에서 의존성을 주입하면 되지 않을까요?

 

아닙니다.

결국에는 의존성을 주입하는 역할을 가진 클래스로 역할분리가 필요합니다.

왜냐하면 의존성을 변경하기 위해서 사용하는 쪽의 클라이언트 코드의 변경이 또 불가피해지기 때문입니다.

또한, 의존성을 주입해주는 역할을 가진 컨테이너가 존재한다면 의존성주입을 한 곳에 모아서 관리하기 때문에 유지보수에 더욱 용이합니다.

 

의존성 주입을 한 곳에 모아서 관리하는 곳을

DI (Dependency Insection) 컨테이너라고 부릅니다.

 

스프링은 DI 컨테이너를 자동으로 생성해주고 구현체를 컨테이너에 등록해 의존관계를 자동으로 주입해줍니다.

 

 

 

그런데 단지 의존성을 컨테이너에서 자동으로 주입해주기 위해서만 스프링을 사용할까요?

 

다음 시간에는 스프링이 관리하는 컴포넌트가 어떤 이점을 가지고 있는지에 대해 알아보도록 하겠습니다.