🔖 프레임워크 vs 라이브러리
프레임워크(FrameWork)
: 프로그램 개발을 위한 여러 요소와 메뉴얼(규약)을 제공하는 프로그램이며, 코드를 제어하고 대신 실행함(ex) JUnit)라이브러리(Library)
: 개발 시 선택적으로 사용 가능한 기능을 제공하는 도구들이며, 작성 코드가 직접 제어의 흐름을 담당
스프링은 자바 기반 프레임워크인 만큼, 순수한 객체 지향 언어의 강점을 살리는 프레임워크이다. 즉, 나머지는 객체 지향을 위한 도구들일 뿐이다.
핵심 기술로는 스프링 DI 컨테이너, AOP, IOC(의존성 역전) 등이 존재한다.
DI, DI 컨테이너 기술로 다형성 + OCP,DIP를 지원함으로써, 클라이언트 코드의 변경 없이 기능 확장이 가능해졌다.
톰캣 같은 웹서버(WAS)를 내장하고 있기 때문에 단독으로 실행 가능한 스프링 애플리케이션 jar
파일로 손쉽게 생성 가능하다.
스프링은 war
파일을 WAS
에 별도로 담아 배포해야 한다.
손쉬운 빌드 구성을 위한 starter
의 종속성을 제공한다.
스프링의 경우 dependency를 설정해줄 때 설정 파일이 매우 길고, 모든 dependency에 대해 버전 관리도 직접 해줘야 한다.
<dependency> // 스프링
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.5</version>
</dependency>
implementation 'org.springframework.boot:spring-boot-starter-web' // 스프링 부트
application.yml
를 통해 설정 파일이 간단해진다.
스프링의 경우 configuration
설정을 할 때도 매우 길고, 모든 어노테이션 및 빈 등록 등을 설정해줘야 한다. 스프링 부트는 @SpringBootApplication
이 있다면 라이브러리를 스캔해서 필요한 모든 클래스들을 빈으로 등록해준다.
🔖 JAR VS WAR
JAR vs WAR 배포의 차이
[Spring Boot]배포 방법 비교 (JAR vs WAR)
객체 지향 프로그래밍의 특징 중 다형성에 대해 알아보자.
다형성
이란, 객체 설계 시 역할(인터페이스)과 구현(역할 수행 객체)으로 분리하는 것을 의미한다. 다형성을 지키면 설계의 유연성과 변경의 편리함을 느낄 수 있다.
즉, 아래 코드와 같이 클라이언트는 대상의 인터페이스에만 의존하여, 내부 구조와 구현 대상의 변경에 영향이 없는 것을 의미한다.
자바 언어는 이러한 다형성을, 오버라이딩을 통해 구현하고 있다.
public class MemberService {
private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
}
MemberRepository
의 자식 클래스들은 부모 타입으로 모두 할당이 가능하다. 이렇듯,
인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있다는 장점이 존재한다. 즉 클라이언트 변경하지 않고, 서버의 구현 기능을 유연하게 변경 가능한 것이 다형성의 본질이다.
스프링은 다형성을 극대화해서 사용할 수 있도록 도와주는데, 예를 들어IOC
와 DI
는 다형성을 쉽게 사용하도록 지원하는 기능이다.
한 클래스는 하나의 책임(변경 이유)만을 가져야한다. 만약 변경이 있을 때 그 파급 효과가 적다면 단일 책임 원칙을 잘 지켰다고 볼 수 있다.
확장에는 열려있으나, 기존 코드의 변경에는 닫혀 있어야 한다는 원칙이다.
다형성의 역할과 구현의 분리를 활용하면, 인터페이스를 구현한 새로운 클래스를 만드는 것은 기존 코드를 변경하는 것이 아니기 때문에 개방-폐쇄 원칙을 지킬 수 있다.
하지만 위에서의 MemberRepository
는 구현 객체를 변경하려면 클라이언트 코드를 변경해야 하는데, 다형성을 지켰지만 OCP는 지키지 못하는 상황이 발생했다.
이렇게 객체를 생성하고, 연결관계를 맺어주는 역할 즉 설정자를 스프링 컨테이터가 해준다.
객체는 프로그램의 정합성을 깨트리지 않으면서 언제나 하위 타입의 인스터스로 변경 가능해야 한다는 원칙이다. 즉, 하위 클래스는 상위 인터페이스의 규약을 다 지켜야 한다.
인터페이스의 단일 책임 원칙으로, 특정 클라이언트 위한 여러개의 인터페이스로 분리해야 한다. 인터페이스가 하나의 역할만 하도록 분리하게 되면 클라이언트도 그에 맞추어서 분리되기 때문에, 결국은 서로의 변경이 다른 인터페이스에 영향을 주지 않게 된다. 또한 인터페이스가 명확해지고, 대체 가능성이 높아진다.
프로그래머는 추상화에 의존해야 하지, 구체화에 의존하면 안된다는 원칙이다. 스프링의 의존성 주입(DI)은 이 원칙을 따르는 것으로, 쉽게 말하면 클라이언트 코드는 구체 클레스가 아닌 인터페이스에만 의존해야 변경의 용이성이 높다는 것을 말한다.
🔖 주의사항
객체 지향의 핵심은 다형성이지만, 다형성만으로는OCP
와DIP
를 지킬 수 없다. 예를 들어, 아래의 코드는 인터페이스와 구현 클래스에 동시에 의존하고 있기 때문에DIP
를 위반하고 있으며 역시 구현 객체가 변경될 때 클라이언트 코드까지 변경되므로OCP
를 위반하게 된다.MemberRepository m = new JdbcMemberRepository()
스프링은 다형성만으로 부품을 갈아끼우듯이 유연하게 개발할 수 없는 문제를, 관심사의 분리를 통해 해결해준다. 클라이언트의 코드 변경 없이 기능을 확장할 수 있어지는 것이다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
하지만, 새로운 기능으로 변경하려면 클라이언트 코드가 변경되는 문제가 발생한다. 현재 역할과 책임을 명확히 분리하고, 다형성 또한 지키고 있다.
OrderServiceImpl
-> FixDiscountPolicy
구체 클래스에 의존하여 DIP
위반
구체 클래스 의존 관계로 인해 RateDiscountPolicy
로 바꾸는 순간 OrderServiceImpl
코드를 변경해야 하기 때문에 OCP
위반
OrderServiceImpl
이 인터페이스(추상)에만 의존하도록 변경하면, 문제가 해결된다.
public class OrderServiceImpl implements OrderService {
private DiscountPolicy discountPolicy;
}
그리고 위와 같이 인터페이스만으로는 실행이 안되니 구현체를 실행하려면, 클라이언트인 OrderServiceImpl
에 DiscountPolicy
의 구현 객체를 대신 생성하고 주입하는 무엇인가 필요하게 되는데 이 작업을 스프링이 관심사의 분리를 통해 대신 해준다.
객체를 생성 및 연결하는 역할(구성)과 실행하는 역할(사용)을 분리하는 작업을 의미한다. 어플리케이션의 전체 동작 방식을 구성하는 어떠한 설정파일을 별도로 만든다면, 객체들은 각 역할에만 집중할 수 있을 것이다.
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;
}
}
public class AppConfig {
//생성자 주입, 리팩토링
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
🔖 AppConfig 역할
1. 애플리케이션 실제 동작에 필요한 구현 객체 생성
2. 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(DI)
즉, 생성자를 통해 어떤 구현 객체가 주입 될지는 오직 외부에서 결정이 되기 때문에 의존 관계에 대한 고민들은 외부에 맡기고 오직 실행에만 집중할 수 있게 되는 것이다. 또한, 이후 어떠한 변경 사유에 대해서도 AppConfig
파일인 구성영역만 변경하면 되기 때문에 클라이언트 코드에 변경이 필요 없다.
public class AppConfig {
public DiscountPolicy discountPolicy(){
//return new FixDiscountPolicy();
return new RateDiscountPolicy(); // 변경 시 구성파일만 수정하면 됌
}
}
AppConfig
도입과 객체 지향의 5가지 원칙을 연관지어서 보면 다음과 같다.
SRP
: 실행하는 책임만 가지고, 구현 객체를 생성하고 연결하는 역할은 구성 파일이 담당하도록 변경하였다.DIP
: 추상화 인터페이스만 의존하도록 변경한 이후 구성 파일이 객체를 대신 생성해서 의존 관계를 주입해주었다.OCP
: 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경을 닫도록 변경하였다. 다형성을 적용하고 DIP
를 지킨다면 OCP
까지 지킬 수 있는 자격을 얻게 된다.그렇다면, 이전까지는 순수 자바 코드였고 이제 본격적으로 스프링으로 들어가보자.
기존 프로그램은 구현 객체가 직접 필요한 객체들을 생성, 연결 및 실행하였다.
프로그램의 제어 흐름을 직접이 아닌 외부에서 관리하는것, 즉 제어가 역전되는것을 의미한다. 즉 Appconfig와 같이 외부에서 제어 흐름을 결정하고, 구현 객체는 실행의 역할만 담당하게 되는 것이다.
Dependency Injection
이란, 외부에서 객체들의 의존 관계를 주입하는 것을 의미한다. 참고로 의존관계는 정적 클래스 의존관계 + 실행 시점에 결정되는 동적 객체 의존 관계 두개로 나눠진다.
실행하지 않아도 코드 분석(import)를 통해 클래스 간 의존관계를 분석 가능한 관계를 의미한다. 클래스 다이어그램을 생성할 수 있지만, 실제 어떤 객체가 주입될지는 분석이 불가능하다.
실행 시점(런타임)에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다. 이렇게 어플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 참조를 전달해 연관 관계를 맺는 방식을 의존 관계 주입이라 한다.
의존관계 주입을 통해 클라이언트의 호출 대상의 인스턴스를 변경 가능하고, 정적 클래스 의존관계의 변경 없이 동적 객체 의존관계만 변경 가능해진다.
AppConfig
와 같이 객체를 생성하고 관리하고, 의존관계 연결을 해주는 것을 DI
컨테이너 혹은 IoC
컨테이너라 한다.
스프링 컨테이너가 바로 DI 컨테이너로써, 빈 관리와 조회를 담당하는 BeanFactory
최상위 인터페이스의 부가 인터페이스(기능)들을 상속받아 구현하고 있다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
}
Configuration
: 해당 어노테이션이 붙은 AppConfig
을 구성 파일로 설정한다.@Bean
: 해당 어노테이션이 붙은 메서드를 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 메서드명이 스프링 빈의 이름이 된다. public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 컨테이너에서 직접 필요한 스프링 빈을 찾아서 사용하도록 변경
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
}
ApplicationContext
: 스프링 컨테이너로써, AppConfig
의 설정(구성)정보를 가지고 @Bean
이 적힌 메소드들을 호출에 반환된 객체를 컨테이너에 등록(스프링 빈)한다.