[김영한 스프링 review] 스프링 핵심 원리 - 기본편 (1)

조갱·2023년 10월 8일
0

스프링 강의

목록 보기
2/16

객체 지향 설계와 스프링

스프링이 탄생하게 된 배경

스프링이 탄생하기 이전, EJB (Enterprise Java Beans, 기업 환경의 시스템을 구축하기 위한 서버측 컴포넌트 모델)가 지금의 Spring 역할을 했다. 하지만 EJB에 수많은 문제들이 있었으니, 바로

  • 특정 환경, 기술에 종속적인 코드
    • 비즈니스 로직보다 종속을 위한 코드가 많아짐
    • 다른 프레임워크로 전환이 거의 불가능
  • 가격이 비쌈
  • 객체지향적이지 않음
  • 자동화 테스트가 어려움

와 같은 문제점들이다.
Spring은 이러한 EJB 컨테이너를 대체하고, 단순함을 더했다.
Hibernate는 EJB Entity Bean 기술을 대체했고, 현재 JPA의 대표적인 구현체이다.

Spring은 POJO (Plain Old Java Object, 자바로 생성하는 순수한 객체)를 기반으로 한다. 이를 통해, 객체 지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고, 필요에 따라 재활용할 수 있게 됐다. 그 이외에도 BeanFactory, ApplicationContext, DI (의존관게 주입), IOC(제어의 역전), AOP (관점 지향 프로그래밍) 을 통해 개발 생산성을 높였다.

객체 지향의 특징

객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지 를 주고받고, 데이터를 처리할 수 있다. (협력)

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

  • 추상화
    • 객체의 공통적인 속성과 기능을 추출하여 정의
  • 캡슐화
    • 서로 관련있는 필드/기능을 하나로 묶고, 외부에서 필요한 데이터만 공개하고 나머지는 숨긴다.
    • 접근 제한자 (private, protected, default, public)을 이용하여 정보(필드, 메소드)를 외부에 노출하거나 은닉한다.
    • 예 : 자동차의 시동버튼의 내부 로직은 매우 복잡하지만, 단순히 시동 ON/OFF 만을 외부에 노출함으로써 사용자는 노출된 시동 ON/OFF 기능만을 사용한다.
  • 상속
    • 기존의 클래스를 재활용하여 새로운 클래스를 작성
    • 공통으로 사용되는 (재사용되는) 필드/메소드를 묶어서 부모클래스로 정의
    • 자식 클래스는 부모 클래스를 상속(extends)받아서, 부모 클래스에 정의된 필드/메소드를 재사용
    • 반복되는 코드를 최소화
  • 다형성 : 스프링과 객체지향에서 가장 중요하다!
    • 객체의 속성이나 기능이 상황에 따라 여러 형태로 변할 수 있다.
    • 인터페이스와 구현체를 사용한다.
    • 역할 (인터페이스)과 실체(구현체, 얼마든지 대체 가능)으로 구분한다.
    • 사람이 음식을 먹는다.
      -> 사람 (역할, 인터페이스) : 나, 팀장님, 친구, 부모님... (실체, 구현체)
      -> 음식 (역할, 인터페이스) : 치킨, 피자, 탕수육 ... (실체, 구현체)
  • 다형성의 한계 :
    • 역할 (인터페이스)가 바뀌면 클라이언트와 서버단의 수정이 필요하다.
    • 인터페이스를 안정적으로 설계하는 것이 중요

좋은 객체 지향 설계의 5가지 원칙 (SOLID)

  • SRP: 단일 책임 원칙(single responsibility principle)
    • 하나의 클래스는 하나의 책임만을 가져야 한다.
    • 하나의 책임이라는것은 모호하다. (클 수도? 작을 수도?)
    • 수정이 최소한으로 일어나는 것이 중요
  • OCP: 개방-폐쇄 원칙 (Open/closed principle)
    • 확장에는 열려있고 변경에는 닫혀있어야 한다.
    • 다형성을 활용
    • 인터페이스를 통해, 새로운 기능은 새로운 클래스를 만들어 상속받아 사용
  • LSP: 리스코프 치환 원칙 (Liskov substitution principle)
    • 정확성을 깨트리지 않으면서, 하위 인스턴스로 변경할 수 있어야 한다.
    • 다형성을 안정적으로 사용하기 위해 지켜야할 원칙
    • 자식 클래스는 부모 클래스의 규약을 지키고, 의도한 대로 실행되어야 한다.
    • 대표적인 예제로 직사각형-정사각형 예제가 있다.
  • ISP: 인터페이스 분리 원칙 (Interface segregation principle)
    • 특정 클라이언트를 위한 인터페이스 여러개가, 범용 인터페이스 하나보다 낫다.
    • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
    • 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
    • 인터페이스가 명확해지고 대체가 쉬워진다.
  • DIP: 의존관계 역전 원칙 (Dependency inversion principle)
    • 구체화가 아닌 추상화에 의존해야 한다.
    • 즉, 구현 클래스가 아닌 인터페이스에 의존해야한다.

스프링은

DI (Dependency Injection) 를 통해 다형성, OCP, DIP 를 가능하게 한다.

스프링 핵심 원리 이해1 - 예제 만들기

회원 기능

  • 회원 도메인 요구사항
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository = new MemoryMemberRepository();

	... 중략
}

주문 기능

  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository = new MemoryMemberRepository();
	private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

	... 중략
}

위 설계의 문제점

  • 인터페이스를 사용은 하지만, 실질적인 구현체를 할당하고 있다.
  • 구현체가 변경되면 클라이언트 (Impl) 코드의 수정이 필요하다.

새로운 할인 정책의 적용

(강의 자료 중)
악덕 기획자: 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률% 할인으로 변경하고 싶어요. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000
원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 거에요!

순진 개발자: 제가 처음부터 고정 금액 할인은 아니라고 했잖아요.

악덕 기획자: 애자일 소프트웨어 개발 선언 몰라요? “계획을 따르기보다 변화에 대응하기를”

순진 개발자: … (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.

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

위 설계는 인터페이스(DiscountPolicy)와 구현체(FixDiscountPolicy, RateDiscountPolicy)를 잘 분리했다.

하지만, 실질적으로는 주문 클라이언트(OrderServiceImpl)은 구현체 (FixDiscountPolicy)를 참조하고 있었다. -> DIP 위반

정률 할인 정책으로 기능을 확장하기 위해서는, 주문 클라이언트(OrderServiceImpl)의 코드를 수정 (FixDiscountPolicy -> RateDiscountPolicy)로 수정해야 한다. -> OCP 위반

오로지 인터페이스에만 의존하도록 코드를 수정해야 한다.

스프링 핵심 원리 이해2 - 객체 지향 원리 적용

인터페이스에 의존하도록 코드 수정

public class OrderServiceImpl implements OrderService {
	private DiscountPolicy discountPolicy;

	... 중략
}

구현체가 없으니 당연히 NPE (Null Pointer Exception)가 발생한다

누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다. -> DI (Dependency Injection)!

AppConfig 사용

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자

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

회원 기능

public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository;
	
    // 아래 생성자에서 의존성을 주입받는다.
    public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
    }
    ... 중략
 }

주문 기능

public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
	
    // 아래 생성자에서 의존성을 주입받는다.
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
	... 중략
}

AppConfig 사용하기

public class App {
	public static void main(String[] args) {
    	// Service를 appConfig에서 가져오는 것이 중요하다.
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();
		OrderService orderService = appConfig.orderService();
		
        Member member = new Member(1L, "memberA", Grade.VIP);
		memberService.join(member);
		
        Member findMember = memberService.findMember(1L);
        Order order = orderService.createOrder(findMember.memberId, "itemA", 10000);
	}	
}
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    • new MemberServiceImpl(...)
    • new MemoryMemberRepository()
    • new OrderServiceImpl(...)
    • new FixDiscountPolicy()
  • AppConfig는 생성자를 통해서 의존성을 주입(Dependency Injection)해준다.
    • new MemberServiceImpl(memberRepository());
    • new OrderServiceImpl(memberRepository(), discountPolicy());

새로운 구조와 할인 정책 적용

이전에는, 새로운 할인 정책을 적용하기 위해서는 클라이언트 (OrderServiceImpl)의 코드를 수정해야 했다.

AppConfig 의 도입 이후로는, 클라이언트(OrderServiceImpl)은 인터페이스만 참조하며, 새로운 할인 정책을 도입하기 위해서는 AppConfig의 코드만 수정하면 된다.

public class AppConfig {
	... 중략
	public DiscountPolicy discountPolicy() {
		// return new FixDiscountPolicy();
		return new RateDiscountPolicy();
	}
}

AppConfig를 사용하기 이전에 발생했던 문제를 다시 살펴보면,

  • 실질적으로는 주문 클라이언트(OrderServiceImpl)은 구현체 (FixDiscountPolicy)를 참조하고 있었다.
    -> 클라이언트는 인터페이스(역할, DiscountPolicy)만을 참조하며, 인터페이스는 구성 영역에서 구현체를 주입해준다. (DIP 위반 해결)

  • 정률 할인 정책으로 기능을 확장하기 위해서는, 주문 클라이언트(OrderServiceImpl)의 코드를 수정 (FixDiscountPolicy -> RateDiscountPolicy)로 수정해야 한다.
    -> 사용 영역이 아닌 구성 영역에서 수정이 발생하므로, 클라이언트(OrderServiceImpl)의 수정은 발생하지 않는다. (OCP 위반 해결)

좋은 객체지향 설계 5가지 원칙 적용

AppConfig를 사용함으로써 SRP, DIP, OCP을 지키게 됐다.

  • SRP: 단일 책임 원칙
    • 기존
      클라이언트 (OrderServiceImpl)가 객체 생성/연결/실행까지 다양한 책임을 가짐
    • 수정 후
      AppConfig가 객체 생성/연결 을 담당
      클라이언트는 객체를 실행하는 책임만 담당
  • DIP: 의존관계 역전 원칙
    • 기존
      클라이언트 (OrderServiceImpl)는 FixDiscountPolicy 구현체 클래스에 의존했다.
    • 수정 후
      클라이언트는 인터페이스 (DiscountPolicy)만 참조하고, AppConfig가 OrderServiceImpl 생성자를 통해 Fix/RateDiscountPolicy를 주입한다.
  • OCP: 개방폐쇄의 원칙
    • 기존
      할인 정책이 변경될 때마다 클라이언트(OrderServiceImpl)의 코드가 변경돼야 했다. (discountPolicy = new Fix/RateDiscountPolicy())
    • 수정 후
      AppConfig가 클라이언트의 DiscountPolicy 에 대한 의존성을 주입하기 때문에, 새로운 할인 정책이 생기더라도 클라이언트 코드의 수정 없이 AppConfig만 수정하면 된다.

DI, IOC, 컨테이너

  • 제어의 역전 (IOC: Inversion of Control)
    • 기존에는 클라이언트(OrderServiceImpl)가 의존성을 생성/연결/실행 했고, 그 코드는 개발자가 작성해야 했다.
    • AppConfig의 도입으로 인해 개발자는 클라이언트 코드의 수정 없이 외부(AppConfig)에서 주입되는 의존성을 참조하여 사용하기만 하면 된다.
      -> 즉, 의존성에 대한 제어가 개발자에서 외부로 역전된다.
    • 스프링에서는 이러한 AppConfig의 기능을 스프링 컨테이너가 지원해준다.
  • 의존관계 주입 (DI: Dependency Injection)
    • 클라이언트(OrderServiceImpl)는 할인 정책을 인터페이스(DiscountPolicy)에만 의존한다.
    • 실질적인 구현체(의존관계)는 외부(AppConfig)에서 주입해준다.
  • IoC 컨테이너, DI 컨테이너
    • AppConfig 처럼 객체를 생성/관리/의존관계 연결을 담당해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
    • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
    • 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.

Reference
김영한 스프링 강의
EJB : https://woongsin94.tistory.com/357
POJO : https://ittrue.tistory.com/211
LSP : https://velog.io/@harinnnnn/OOP-객체지향-5대-원칙SOLID-리스코프-치환-원칙-LSP
객체지향 특징 : https://www.codestates.com/blog/content/객체-지향-프로그래밍-특징

profile
A fast learner.

0개의 댓글