과정을 즐기자

스프링을 사용하는 이유 본문

Spring

스프링을 사용하는 이유

320Hwany 2022. 11. 17. 13:11

좋은 객체 지향 설계

먼저 좋은 객체 지향 설계의 5가지 원칙 SOLID에 대해 알아보자

  1. SRP(single responsibility priciple)
    단일 책임 원칙으로 한 클래스는 하나의 책임만 가져야 한다.
  2. OCP(open/closed principle)
    개방-폐쇄 원칙으로 확장에는 열려 있으나 변경에는 닫혀있어야 한다.
  3. LSP(Liskov subsititution principle)
    리스코프 치환 원칙으로 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야한다.
  4. ISP(interface segregation principle)
    인터페이스 분리 원칙으로 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  5. DIP(Dependence inversion principle)
    의존 관계 역전 원칙으로 구현 클래스에 의존하지 않고 인터페이스에 의존해야 한다.

자바는 객체지향언어로 다형성을 이용해 확장성있는 설계를 할 수 있다.
하지만 다형성 만으로는 OCP, DIP 원칙을 지키지 못한다.

다음과 같이 OrderServiceImpl은 구현 클래스에 의존하지 않고 인터페이스에 의존해야 한다.
하지만 OrderSeriveImpl 안에서 직접 FixDiscountPolicy, RateDiscountPolicy 중 어떤 것인지 직접 의존관계를 주입해주어야 한다. 이렇게 하면 OCP, DIP 원칙을 지키지 못하게 된다.

이를 해결하려면 관심사를 분리하면 된다. 즉 의존 관계를 주입해주는 무언가를 따로 만들면 된다.
예를 들어 AppConfig라는 클래스를 만들어서 구현 객체를 생성하고 연결하는 책임을 가지게 하면 된다.
이렇게 하면 OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존한다.
OCP, DIP 원칙을 지킬 수 있다.
이렇게 클라이언트 코드와 객체를 생성하고 구성하는 configuration으로 영역을 분리할 수 있다.

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();
      }
}

IoC, DI

IoC는 Inversion of Control의 약자로 제어의 역전이라는 뜻이다. 프로그램의 제어 흐름을 직접 제어하는 것이 아니라
외부에서 관리하는 것을 제어의 역전이라고 한다.

DI는 Dependency Injection의 약자로 의존관계 주입이라는 뜻이다. 외부에서 의존 관계를 주입해주는 것을 말한다.
따라서 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의
타입 인스턴스를 변경할 수 있다. 위에서 만든 AppConfig 클래스를 IoC 컨테이너, DI 컨테이너라고 한다.

스프링 시작하기

위와 같이 매번 개발자가 AppConfig 클래스를 사용해서 의존관계를 주입하는 것은 번거롭다.
스프링을 사용해서 이를 해결해보자.

@Configuration
public class AppConfig {

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

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

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

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

AppConfig 클래스에 @Configuration, @Bean 애노테이션을 사용해서 스프링 컨테이너에 스프링 빈으로 등록을 한다.
이렇게 스프링을 사용하는 것의 장점이 무엇일까?

스프링 컨테이너(Application Context)

다음과 같이 스프링 컨테이너를 생성할 수 있다.

ApplicationContext applicationContext = new AnnotationApplicationContext(AppConfig.class);

스프링 컨테이너는 AppConfig 클래스의 빈 정보를 보고 스프링 빈으로 등록을 해준다. 생성자를 통해 의존관계도 주입된다.
그냥 개발자가 직접 AppConfig로 설정을 할 때와 달리 스프링을 사용하면 싱글톤으로 객체를 생성한다.
즉 클래스의 인스턴스가 딱 1번만 생성되고 공유한다. 직접 개발자가 싱글톤 패턴을 위한 코드를 작성하지 않아도 싱글톤으로 관리한다.

이를 위해 스프링은 @Bean이 붙은 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고
스프링 빈이 존재하지 않다면 스프링 빈을 생성하여 등록한다.
@Configuration을 붙여주어야 스프링 빈이 싱글톤으로 보장된다.

컴포넌트 스캔(ComponentScan)

위의 예시에서 싱글톤으로 생성해주는 장점이 있었지만 아직 개발자가 직접 작성해야 할 코드가 많다.
AppConfig에 작성한 코드들을 @ComponentScan, @Component를 사용해서 줄일 수 있다.
관리해주는 클래스 위에 @ComponentScan이라고 작성해주고 스프링 빈으로 등록할 클래스 위에
@Component라고 작성한다. 의존관계 자동주입은 @Autowired를 통해서 할 수 있다.

스프링을 사용하여 관리해주는 클래스의 코드도 줄이고 객체들을 싱글톤으로 관리하며 의존관계 자동 주입도 해준다!

생성자 주입

@Repository, @Service, @Controller 안에는 @Component가 기본적으로 들어가 있어 컴포넌트 스캔의 대상이 된다. 또한 의존 관계 자동 주입은 주로 생성자를 통해서 한다. 생성자를 통해서 하면 테스트 코드를 작성할 때 어떤 의존관계가 빠졌는 지를 빠르게 파악할 수 있다.

@Autowired
public MemberController(MemberService memberService) {
    this.memberService = memberService;
}

또한 더 나아가서 @RequiredArgsContructor를 쓰면 @Autowired를 사용하지 않아도 자동으로 의존 관계를 주입해준다.

이 어노테이션은 초기화되지 않은 final 필드나 @NotNull이 붙은 필드에 대해 생성자를 만들어준다.

private final MemberService memberService;
private final PostService postService;

출처 : 스프링 핵심 원리 기본편 (김영한)