스프링의 진짜 핵심

MinseokGO·2023년 7월 20일

스프링

목록 보기
4/5

스프링은 왜 사용될까?

  • 스프링은 자바 언어 기반의 프레임워크
  • 자바 언어의 가장 큰 특징 : 객체 지향 언어
  • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

"스프링은 자바 엔터프라이즈 어플리케이션을 잘 개발할 수 있도록 도와주는 프레임워크"

그럼 객체 지향의 특징이 뭔데?

  • 추상화
  • 캡슐화
  • 상속
    ...
  • 다형성

이러한 특징들로 객체 지향 프로그래밍은 프로그램을 객체간의 협력을 통해 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

역할과 구현으로 세상을 구분하자

실세계와 객체 지향을 1 : 1로 매칭하지 않고 역할과 구현으로 구분하자.

자동차는 차종이 바뀌어도 자동차의 역할(운전자의 이동을 도와줌)은 바뀌지 않는다. 운전자는 이러한 자동차의 내부를 몰라도 된다. 가령 자동차의 내부가 바뀐다 해도 자동차의 역할은 바뀌지 않고 그대로이다.

어떤 차량은 내부에 에이컨이 없고 또 다른 어떤 차량은 내부에 에어컨이 있다. 이렇듯 자동차의 역할은 그대로이며, 부수적인 기능들이 구현될 수 있는 것이다.

자동차에 이러한 기능들이 구현되었다고 해서, 운전자에게 영향을 주는 것은 아니다. 결국 역할구현으로 세상을 구분함으로써 클라이언트에게 영향을 주지 않고 새로운 기능을 제공할 수 있는 것이다.

해당 논리를 자바 언어에 접목시켜보자

먼저 다형성이란 간단하게 여러가지 형태를 갖는 것을 말한다. 자바 언어의 다형성을 활용해서 역할과 구현을 분리해보자.

  • 역할 == 인터페이스
  • 구현 == 인터페이스를 구현한 클래스, 구현 객체, 실제 구현체

객체 설계 시, 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만들자.

위 사진의 경우 MemberRepository라는 역할을 부여하고, 그 역할을 수행하는 구현 객체인 MemoryMemberRepository와 JdbcMemberRepository를 만들었다.

그리고 MemberService는 Repository라는 객체에 의존하고 있다!

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

자바 언어의 다형성을 이용함으로써 In-Memory DB에서 실제 저장소를 갖고 있는 DB로 바뀌게 되면 주석 처리된 코드로 갈아끼우기만 하면 되는 편리함을 얻을 수 있다. 그런데 좋은 객체 지향 설계는 이 정도의 편리함을 싫어한다.

바로 SOLID의 D(D.I.P)를 위배한다는 것..! (Dependency Inversion Principle : 의존 관계 역전의 원칙)

"객체 지향 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다."

라는 말이 있듯이, 클라이언트(MemberService)는 인터페이스에 의존해야지 실제 구현체에 의존하면 안된다는 것이다. 위 코드는 클라이언트가 직접 구현 클래스를 선택하고 있다. 이 말인 즉슨, 클라이언트가 구현체에 의존을 하고 있다는 것이다..

해당 코드는 또 다른 문제점을 가지고 있는데, SOLID의 O(O.C.P)를 위배한다. (Open-Closed Principle : 개방-폐쇄 원칙)

프로그램이 기능 확장에는 열려있어야 하며, 수정에는 닫혀 있어야 한다는 원칙이다. 만약 해당 프로그램의 DB를 변경해야한다면? 당연히 MemberService에 작성된 코드도 변경되어야 한다. 바로 프로그램의 수정이 일어난다는 것!

그리고 한 가지의 문제점이 더 존재한다. SOLID의 S(S.R.P)를 위배한다. (Single Responsibility Principle : 단일 책임의 원칙)

하나의 객체는 하나의 책임만을 가지고 있어야 한다는 원칙이다. 언뜻보면 음.. 다른 책임이 보이지 않는데? 싶을 수도 있지만, 우리가 알고있는 비즈니스 로직의 Service단은 Repository단과 상호작용하며 데이터를 가공하고 서비스 로직을 실행하는 단이다.

그럼 여기서 MemberService는 서비스 로직을 수행하는 책임과 구현체를 직접 선택하는 책임을 가지고 있다..! 단일 책임의 원칙에 어긋난 것이다. 그럼 해당 문제들을 어떻게 해결할 수 있을까?

제어의 역전(IoC)을 구현하자!

어떻게 보면 관심사를 분리하는 게 더 맞을 수도 있지만 나는 해당 파트를 공부하면서 제어의 역전이 자연스럽게 구현되는 것을 확인했다. 어떻게 제어의 역전이 구현될 수 있었을까?

  1. 단일 책임의 원칙을 준수하기 위해 관심사(책임)를 분리하고자 했다.
  2. Service의 순기능은 그대로 두고, 구현체를 직접 선택하는 로직을 제거해야만 한다.
  3. 구현체를 직접 생성하고 연결하는 객체를 만들어주면 되지 않을까?

맞다. 설정(Config) 파일을 만들어 객체의 생성과 연결을 담당하도록 책임을 분리하는 것이다.

//AppConfig.java
public class AppConfig {
	public MemberService memberService() {
    	return new MemberServiceImpl(new MemoryMemberRepository());
        //return new MemberServiceImpl(new JdbcMemberRepository());
    }
}

//MemberService.java
public class MemberService {
	private MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
}

바로 이렇게 외부에서 클라이언트(MemberService)가 의존하는 객체(MemberRepository)를 주입하는 방법을 택한다. 이 방식을 DI(Dependency Injection : 의존관계 주입)라고 한다.

보통 객체를 사용하려면 객체를 사용하려는 곳에서 객체를 생성하지만, 그렇지 않고 외부에서 객체를 생성해주는 방식을 '객체 제어의 주체가 역전되었다.' 라고 하여 제어의 역전(Invesion of Control)이라고 하며 제어의 역전을 구현하는 방법이 위에서 설명한 의존관계 주입이다.

하지만 위 코드에서 조금 불편한(?) 부분이 남아있는 것 같다. 바로 Repository에 대한 의존 주입 과정에서 new 생성자를 통해 주입을 하게 되는데, 이는 MemberRepository의 생성과 연결에 대한 역할이 뚜렷하지 않다고 볼 수 있다. 또한 MemberService에 주입되는 Repository의 종류가 바뀌게 되면 해당 코드를 수정해야하는 대참사가 일어난다..

//AppConfig.java
public class AppConfig {
	public MemberService memberService() {
    	return new MemberServiceImpl(memberRepository());
    }
    
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
        // return new JdbcMemberRepository();
    }
}

그래서 이렇게 더 세부적으로 역할을 분리하자. 해당 프로그램의 DB가 바뀌더라도 memberRepository() 메서드만 바뀐 DB로 갈아끼우면 다른 객체들의 의존 주입이 해결된다. 역할을 나눔으로써 OCP를 준수하게 된 것이다.

설정 클래스 파일을 보면 메서드들의 return 형도 구현체에 의존하지 않고 인터페이스에 의존한 것을 확인할 수 있다. 만약 return 형을 구현체로 지정했다면, DIP, OCP를 둘다 위배하는 상황이 발생할 것이다. 이유는 간단하다. 바로 구현체가 바뀌면 코드도 바뀌어야 한다는 것..!

이제 다시 MemberService 코드를 살펴보자.

//MemberService.java
public class MemberService {
	private MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
    
    ... 서비스 로직 수행 코드 ...
}

많이 달라졌다. 기존의 Service 코드는 DIP, OCP, SRP를 모두 위배하고 있었다. 하지만 바뀐 코드를 살펴보자.

  1. 구현체에 의존하지 않고, 인터페이스에 의존하고 있다. -> DIP 준수
private MemberRepository memberRepository;
  1. memberRepository의 구현체가 바뀌더라도 클라이언트(MemberService) 코드를 수정하지 않아도 된다. -> OCP 준수
private MemberRepository memberRepository;
  1. 외부에서 객체를 생성하고 주입받음으로써 구현체를 직접 선택하는 책임이 사라지고 서비스 로직만 수행하는 책임만 가진다. -> SRP 준수
public MemberService(MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

관심사의 분리, 더 자세하게는 제어의 역전의존 주입을 통해 구현함으로써 세가지 문제점을 단번에 해결했다.

좋은 객체 지향 설계라는 말이 괜히 존재하는 게 아닌 것 같다. 설계를 잘 해두면 코드 유지보수가 정말 쉬운 것 같다..

다음 포스팅은 웹 어플리케이션에서 고객의 동시 요청에 따른 무분별한 객체 생성에 대한 주제로 작성해보겠습니다!

profile
초보임당

2개의 댓글

comment-user-thumbnail
2023년 7월 20일

훌륭한 글이네요. 감사합니다.

1개의 답글