스프링 프레임워크(줄여서 스프링)은 오픈 소스 기반의 기술로, Java 기반의 웹 애플리케이션을 만드는 개발자가 되기 위해서 반드시 익혀야 하는 기술이다.
먼저 프레임워크란, 개발자가 애플리케이션의 핵심 로직을 개발하는 것에 집중할 수 있도록 기본적인 프로그래밍을 위한 틀이나 구조를 제공하는 것이다. 프레임워크를 사용하면 개발자가 애플리케이션의 사소한 부분까지 일일이 개발할 필요없이 프레임워크에서 제공하는 라이브러리를 활용해 다양한 기능을 구현할 수 있다.
(주도권의 내용은 아래 특징에서 IoC로 자세히 알아볼 예정)
먼저 기업에서 애플리케이션 개발에 있어 Framework를 선택할 때,
이 두 가지 사항을 주요하게 고려한다(고 한다). 스프링 프레임워크는 객체 지향 설계 원칙에 잘 맞는 재사용과 확장이 가능한 애플리케이션 개발을 할 수 있게 해준다.
스프링이 도입되기 전에는,
POJO = 평범한 Java 객체
POJO 프로그래밍
규칙
Java나 Java의 스펙에 정의된 것 외의 다른 기술, 규약에 얽매이지 않아야 한다.
(특정 기술을 상속해서 코드를 작성하면, 이후 애플리케이션의 요구사항이 변하거나 다른 기술로 변경하고자 할 때 코드를 일일이 수정/제거해야 한다. 또한 Java는 다중상속을 지원하지 않아 extends
키워드로 한 번 상속을 하게되면 상위 클래스를 받아서 하위 클래스를 확장하는 객체지향 설계 기법을 적용하기 어려워진다.)
특정 환경에 종속적이지 않아야 한다.
(시스템 요구 사항이 변경될 경우 코드에서 특정 환경에 종속하는 코드를, 심하면 애플리케이션 전부를 뜯어고쳐야 한다.)
POJO 프로그래밍을 해야 하는 이유
스프링은 POJO 프로그래밍을 지향하는 프레임워크다. 이를 위해 세 가지 기술을 지원하는데, 그것들은 바로 스프링 삼각형에서 POJO를 감싸고 있는 IoC/DI, AOP, PSA이다.
우리말로는 '제어의 역전'이라고 부른다.
public class Example {
public static void main(String[] args) {
System.out.println("IoC");
}
}
일반적으로 Java 콘솔 애플리케이션을 실행하려면 main()
메서드가 있어야 한다. 위 코드를 실행시키면 main() 메서드가 호출되고, System 클래스 -> out 변수 -> println() 순으로 호출이 된다. 이렇게 개발자가 작성한 코드를 순차적으로 실행하는 게 일반적인 제어 흐름이다.Java 콘솔 애플리케이션이 아닌, 웹 상에서 돌아가는 Java 웹 애플리케이션의 경우에는 main()
메서드가 없다. 클라이언트가 외부에서 접속하는데 애플리케이션 이용중 main() 메서드가 종료되면 안 되기 때문이다.
때문에 서블릿 컨테이너라는 거대한 상자에 서블릿 사양에 맞게 작성된 서블릿 클래스만 존재한다. 클라이언트가 요청을 하면 서블릿 컨테이너 내의 컨테이너 로직(service()
메서드)이 서블릿을 실행시켜준다.
이 경우 서블릿 컨테이너가 서블릿을 제어하기 때문에 애플리케이션의 주도권이 서블릿 컨테이너에 있고, 서블릿과 웹 애플리케이션 간의 IoC(제어의 역,전) 개념이 적용되었다고 한다. 스프링에 적용된 IoC 개념은 아래에서 살펴볼 DI이다.
우리말로는 '의존성 주입'이라고 부른다.
IoC는 서버 컨테이너 기술, 디자인 패턴, 객체 지향 설계 등에 적용되는 일반적인 개념이다. 반면 DI는 제어의 역전을 좀 더 체화 시킨 것으로 볼 수 있다.
의존성
클래스 A가 클래스 B의 기능을 사용할 때, 클래스 A는 클래스 B에 의존한다.
클래스 A가 클래스 B의 객체를 생성해서 참조할 때, 클래스 A는 클래스 B에 의존한다.
이와 같이 하나의 클래스가 다른 클래스의 객체, 기능 등을 사용할 때 의존관계가 성립된다고 한다.
의존성 주입 (!=의존 관계 성립)
생성자를 통해서 어떤 클래스의 객체를 전달 받는 것. 생성자의 파라미터로 객체를 전달하는 것을 외부에서 객체를 주입한다고 말하는 것이다.
(의존성 주입의 방법은 다양하지만 가장 많이 사용되는 방법이 생성자를 이용한 DI이다.)
코드로 예시를 들면 아래와 같다.
// 의존 관계 성립의 예
public class Calculator {
public static void main(String[] args) {
Operation operation = new Operation(); // Operation 클래스에 의존
List<Op> operationList = operation.getOperationList();
}
}
public class Operation {
public List<Op> getOperationList() {
return null;
}
}
// 의존성 주입의 예 (생성자 이용)
public class CalculatorClient {
public static void main(String[] args) {
Operation operation = new Operation();
Calculator calculator = new Calculator(operation);
// CalculatorClient 클래스가 Calculator의 생성자 파라미터로 operation을
// 전달하고 있기 때문에 객체를 주입해주는 "외부"가 되어줌
public class Calculator {
private Operation operation;
public Calculator(Operation operation)
//Calculator의 생성자로 Operation의 객체를 전달 받음 = 의존성 주입
{
this.operation = operation;
}
public List<Op> getOps() {
return operation.getOperationList();
}
public class Operation {
public List<Op> getOperationList() {
return null;
}
}
new
키워드로 의존 객체를 생성하고 다른 클래스에 주입했을 때, 클래스 간 결합은 해진다. 이러한 클래스가 수 백가지라고 쳤을 때 수정 요청이 들어오면 클래스마다 수동으로 수정해줘야 해서 매우 비효율적이다.public class CalculatorClient {
public static void main(String[] args) {
Operation operation = new OperationStub();
// new로 OperationStub 클래스의 객체를 생성해서
// Operation 인터페이스에 할당 (업캐스팅)
// 업캐스팅을 통한 의존성 주입으로 인해
//Calculator와 Operation은 느슨한 결합 관계
Calculator calculator = new Calculator(operation);
public class Calculator {
private Operation operation;
public Calculator(Operation operation)
// Operation 인터페이스를 가리키는 파라미터
{
this.operation = operation;
}
public List<Op> getOps() {
return operation.getOperationList();
}
public interface Operation {
List<Op> getOperationList();
}
public class OperationStub implements Operation {
@Override
public List<Op> getOperationList() {
return List.of(
...
);
}
}
여기서 new
키워드를 최소화하여 의존 관계를 더 느슨하게 해야 하는데, 이 부분은 추후 DI 개별 포스팅으로 살펴보겠다.
AOP는 우리말로 관심 지향 프로그래밍이라고 봐도 된다. 여기서 관심은 interest가 아닌 Aspect다.
애플리케이션 개발시 어떤 사항이 비즈니스 로직 달성을 위한 것인지에 따라 개발하는 사항을 크게 두 분류로 나눌 수 있다.
핵심 관심 사항(Core concern)
= 비즈니스 로직
= 애플리케이션의 주목적 달성을 위한 핵심 로직
(위 표에서 Presentation, Business, Data Access 레이어에 해당)
공통 관심 사항(Cross-cutting concern)
= 애플리케이션 전반에 사용되는 공통 기능
(위 표에서 로깅, 보안, 트랜잭션에 해당)
AOP는 애플리케이션의 핵심 업무 로직에서 공통 기능 로직을 분리하는 것이다. 분리하는 이유는 다음과 같다.
코드를 보다 깔끔하게 쓰고, 재사용성을 높이려는 고민이 반영된 프로그래밍이 결국 객체지향적 프로그래밍이다.
PSA는 한 마디로 추상화 개념이다.
객체지향의 4가지 개념 중 하나인 추상화를 이미 이전 포스팅을 통해 살펴봤기 때문에 간단히 설명하자면,
추상화를 하면 클라이언트는 요청하는 수만큼 다른 개별 클래스를 보는 게 아니라, 추상 클래스만 일관되게 바라보며 하위 클래스의 기능을 쓸 수 있다.
public interface Student {
String getHomeWork();
}
class StudentA implements Student {
public String getHomeWork() {
return "two pages of essay";
}
}
class StudentB implements Student {
public String getHomeWork() {
return "ten math questions";
}
}
추상화의 가장 기본적인 예시 코드이지만, 이것을 서비스에 접목해서 생각하면 클라이언트가 바로 StudentA나 StudentB 클래스에 접근하는 것이 아닌, Student라는 인터페이스를 환승장처럼 중간에 끼워두고 그 구현체라면 어디든 접근할 수 있게 해주는 것이다.
필요한 리소스에 바로 접근하는 방식 대신 추상화를 통해 인터페이스를 거쳐가게 하는 이유는 이 방식이 서비스의 기능에 접근하는 방식 자체를 일관되게 유지하기 때문이다. 또한 기술 사용이 유연해진다. 이것이 PSA(일관된 서비스 추상화)다.
기술 사용이 유연하다는 것은, 애플리케이션의 요구 사항이 변경됐을 때 대처를 유연히 할 수 있다는 뜻이다. 모두 직접 연결시켰다면 일일이 변경하는 수고를 치뤄야하나, 접근 방식을 일관되게 통일시켜놓았으니 최소한의 변경만으로 요구 사항을 반영할 수 있다.