Framework : Application을 개발하기 위한 규약과 다양한 요소들을 제공하는 틀
Library : Application 개발 시 활용가능한 도구(코드)의 집합
Caller : 호출함
Callee : 호출 당함
size : Package < Module < Library
컴퓨터의 운영체제는 프로그램의 실행을 위해 다양한 메모리 공간을 제공한다. 대표적인 메모리 공간은 4가지이다.
- 코드(code) 영역
- 데이터(data) 영역
- 스택(stack) 영역
- 힙(heap) 영역
메모리의 코드 영역은 실행할 프로그램의 코드가 저장되는 영역으로 텍스트 영역이라고도 부른다.
CPU는 코드 영역에 저장된 명령어를 하나씩 가져와서 처리하게 된다.
프로그램이 시작하고 종료될 때 까지 메모리에 계속 남아 있는다.
프로그램이 종료될 때까지 지워지지 않을 데이터가 저장된다.
메모리의 데이터 영역은 프로그램의 전역 변수와 정적(static) 변수, 상수가 저장되는 영역이다.
프로그램의 시작과 함께 할당되며 프로그램이 종료되면 소멸한다.
스택 영역은 프로그램이 자동으로 사용하는 임시 메모리 영역이다.
함수 호출 시 생성되는 지역변수와 매개변수가 저장되는 영역이고 함수 호출이 완료되면 사라진다.
이때 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임(stack frame)이라고 한다.
함수의 호출하는 위치도 저장된다.
스택 영역은 푸시(push) 동작으로 데이터를 저장하고 팝(pop) 동작으로 데이터를 인출한다.
이는 스택 영역이 메모리의 높은 주소에서 낮은 주소의 방향으로 할당 되기 때문이다.
위의 그림으로 설명하면, 가장 아래(높은 주소)부터 차곡차곡 위(낮은 주소)의 방향으로 데이터가 쌓인다고 생각하면 된다.
이러한 스택은 후입선출(LIFO, Last-In First-Out) 방식에 따라 동작하므로 가장 늦게 저장된 데이터가 가장 먼저 인출된다.
-> pop으로 인출하니까 리포 방식인 것.
힙보다 빠르다.
스택보다 큰 메모리를 할당받기 위해 사용된다.
프로그래머가 직접 공간을 할당, 해제하는 메모리 공간이다.
힙 영역은 선입선출(FIFO, First-In First-Out)의 방식으로, 가장 먼저 들어온 데이터가 가장 먼저 인출 된다.
이는 힙 영역이 메모리의 낮은 주소에서 높은 주소의 방향으로 할당되기 때문이다.
위의 그림으로 설명하면, 위(낮은 주소)부터 차곡차곡 아래(높은 주소) 방향으로 데이터가 쌓인다고 생각하면 된다.
스택보다 느리다.
오버 플로우란 영어로 넘쳐흐른다는 뜻 이다.
말 그대로, 한정된 메모리 공간이 부족하여 메모리 안에 있는 데이터가 넘쳐 흐르는 현상이다.
오버 플로우의 종류 중에 힙 오버 플로우와 스택 오버 플로우가 있다.
힙은 메모리 위쪽 주소부터 할당되고, 스택은 메모리 아래쪽 주소부터 할당되기 때문에 각 영역이 상대 공간을 침범하는 일이 발생할 수 있다.
이때 힙이 스택을 침범하는 경우를 힙 오버 플로우라 하고, 스택이 힙을 침범하는 경우를 스택 오버 플로우라고 한다.
Web Application을 만들기 위해 필요한 요구사항
- 유저 혹은 Frontend Application의 요청을 처리하고, 적절한 응답을 줄 수 있어야 한다.
- 예외 처리를 할 수 있고, 예외가 발생했을 때 적절한 응답을 줄 수 있어야 한다.
- 인증과 인가 처리를 할 수 있어야 한다.
- 비즈니스 로직을 처리할 수 있어야 한다.
- Transaction 관리 전략이 있어야 한다.
- 스토리지 및 다른 외부 시스템과 통신할 수 있어야 한다.
트랜잭션(Transaction 이하 트랜잭션)이란, 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다. 간단하게 말해서 아래의 질의어(SQL)를 이용하여 데이터베이스를 접근 하는 것을 의미한다. SELECT, INSERT, DELETE, UPDATE 이용.
위 요구사항들을 처리하기 위해 스프링을 통해 개발할 시 일반적으로 레이어를 나눈다.
레이어를 나누는 이유는 관심사의 분리(seperation of concerns)를 위해서이다. 관심사의 분리를 통해서 코드의 재사용성, 유지보수성을 높일 수 있다. 레이어를 나누는 방식은 일반적으로 Web Layer, Service Layer, Repository Layer로 구성한다.
- Web Layer
어플리케이션의 최상위 레이어.
클라이언트의 요청을 받고 응답을 주는 역할(e.Controllers).
하위 레이어에서 발생한 예외들을 처리하여 적절한 응답을 준다(e.Exception Handlers).
인증과 인가 처리를 담당(e.Filters).
- Service Layer
web layer 하위에 존재하는 레이어.
Transaction 경계의 역할.
Application Service, Infrastructure Service로 나뉘어진다.
(1) Application Service: 요청의 처리에 대한 주요 로직을 담당하고, 최종적으로 응답을 WebLayer에 넘겨주는 역할.
(2) Infrastructure Service: 데이터 베이스, 이메일 서버 같은 외부 서비스와 통신하는 역할.
Application Service에서 Infrastructure Service를 사용.
- Repository Layer
가장 하위에 존재하는 Layer.
데이터베이스와 통신하는 역할을 담당.
Web Layer가 Service Layer를 호출하고, 다시 Service Layer에서 Repository Layer를 호출하는 구조.
크게 보자면 DTO와 Domain Model 이 각 Layer사이의 변수로 넘겨진다고 볼 수 있다.
DTO(Data Transfer Object)
데이터를 담을 수 있는 간단한 객체.
Application 내에서 각 Layer 사이의 데이터를 전달하는데 쓰인다.
응답과 요청 또한 DTO로 표현될 수 있다.
Domain Model
Domain Service, Entity, VO(Value Object)를 포함하는 개념.
(1) Domain Service : 도메인에 대한 특정 로직을 수행하는 Stateless 한 Class. 여기서 도메인은 우리가 만드는 Application을 통해 해결하고자 하는 문제 / 분야임.
(2) Entity : Database에 저장될 수 있는 Data를 표현하는 객체로, table과 대응. Entity의 Instance는 Table의 한 Row와 대응.
(3) VO(Value Object) : 값을 표현하는 객체로, Entity와 Lifecycle을 함께한다. Entity가 갖고 있는 속성중 하나라 할 수 있다.
위 그림처럼, DTO는 Web Layer와 Service Layer에서 사용.
Web Layer는 DTO를 input으로 받고, DTO를 응답. 각각 Request와 Response에 대응.
Service Layer는 Web Layer로부터 DTO 혹
은 Basic Type(Int, String …)을 넘겨 받아 로직을 수행한 후 다시 DTO를 WebLayer에 응답.
Service Layer는 내부에서 로직을 수행하기 위해 Domain Model을 사용.
Repository Layer는 Entity와 BasicType 을 파라미터로 받고, Entity를 응답
DispatchServlet의 역할이 중요!
- DispatchServlet은 FrontController 라고도 한다. 앞단에서 사용자의 요청을 컨트롤하여 다른 컨트롤러에 전달하기 때문이다.
- 위 그림에서 DispatchServlet, HandlerAdapter, HandlerMapping은 Spring에서 제공하는 영역.
- Controller, Service, Repository는 우리가 구현하는 영역.
Client는 Dispatch Servlet에 Request요청. 여기서 Request HTTP Method로 GET, POST, PUT, PATCH, DELETE와 URL, 그리고 보내는 JSON Data를 포함.
Dispatch Servlet은 HandlerMapping을 통해 요청에 대응되는 Controller를 검색하고, 응답을 받는다.
2에서 HandlerMapping에게 요청해 검색해서 해당하는 Controller에게 Request에 대한 처리를 요청.
Controller, Service, Repository를 거쳐 비즈니스 로직을 수행한 후 Client가 요청한 Response를 응답.
다시 Handler Adpater가 응답을 Dispatch Servlet에 전달.
최종적으로 HTTP Response 형태의 응답이 Client에 전달된다.
우리가 작성하는 Controller, Service, Repository들이 Plumbing이 된다.
객체가 자체적으로 필요로 하는 의존성을 생성하는 것이 아니라, 외부에서 주입받는 디자인 패턴.
객체간의 결합도를 낮추기 위해 사용.
주로 Constructor 기반 주입, Field 기반 주입, Setter 기반 주입이 있다.
아래는 DI 가 적용되지 않은 예시. 내부에서 의존성을 생성하고 있다.
class Dependent() {
private val dependency = Dependency(arg1, arg2)
}
Constructor를 통해 주입받는 예시
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
Field를 통해 주입 받는 예시
class Dependent() {
lateinit var dependency: Dependency
}
Setter를 통해 주입받는 예시
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
A가 B에 의존한다. B가 변경되면 A의 내용도 변경된다. 하지만 의존 대상을 직접 생성(결정)하는 것이 아니라 외부로부터 주입받는다. 의존성을 외부로부터 주입받는다고 할 수 있다. 주입 대상인 B 인스턴스들간의 상태 차이만 있을 뿐이지만 만약 B를 인터페이스로 추상화하고 하면 다양한 구현체가 들어옴으로써 의존성을 다각화할 수 있다.
현업에서는 Constructor를 통한 주입을 사용한다.
<이유>
1. 의존되는 객체의 불변성 확보(val)
2. 순환 참조 방지(순환참조는 맞물리는 DI(Dependency Injection)상황에서 스프링이 어느 스프링 빈을 먼저 생성할지 결정하지 못해 발생)
3. 테스트 코드 작성의 용이
(DI ⊂ IoC)
DI 없이도 IoC를 만족하는 프로그램을 만들 수 있다. IoC는 프로그램 제어권을 역전시키는 개념이고, DI는 해당 개념을 구현하기 위해 사용하는 디자인 패턴 중 하나로, 이름 그대로 객체의 의존관계를 외부에서 주입시키는 패턴을 말한다.
좀 더 자세히 이해해보기 위해 프레임워크를 적용하지 않은, 우리가 그동안 작성해왔던 일반적인 프로그램을 생각해보자. 객체의 생명주기(객체의 생성, 초기화, 소멸, 메서드 호출 등)를 클라이언트 구현 객체가 직접 관리한다. 또한 다른 사람이 작성한 외부 코드(라이브러리)를 호출하더라도 해당 코드의 호출 시점 역시 직접 관리한다. 하지만 이러한 생명주기를 직접 관리하지 않는 경우라면?
스프링과 같은 프레임워크를 사용할 때를 생각해보자. Controller, Service 같은 객체들의 동작을 우리가 직접 구현하기는 하지만, 해당 객체들이 어느 시점에 호출될 지는 신경쓰지 않는다. 단지 프레임워크가 요구하는대로 객체를 생성하면, 프레임워크가 해당 객체들을 가져다가 생성하고, 메서드를 호출하고, 소멸시킨다. 프로그램의 제어권이 역전된 것이다.
때문에 프레임워크와 라이브러리는 어떤 차이가 있는지에 대해 IoC를 통해 설명이 가능하다. 라이브러리를 사용하는 어플리케이션은 어플리케이션의 제어 흐름을 라이브러리에 내주지 않는다. 단지 필요한 시점에 라이브러리에 작성된 객체를 적재적소에 가져다 쓸 뿐이다. 하지만 프레임워크를 사용한 어플리케이션의 경우, 어플리케이션 코드에 작성한 객체들을 프레임워크가 필요한 시점에 가져다가 프로그램을 구동하기 때문에 프로그램의 제어권이 프레임워크로 역전된다.
프레임워크에서 IoC를 제공할 때 IoC Container라고 부른다. pring에서는 IoC의 방식중 DI 를 주로 사용하기 때문에, 최근에는 DI Container라고 불리기도 한다.
결국 우리는 이 DI Container를 통해서, 우리가 작성한 Class의 관리를 크게 신경쓰지 않고, Spring에 맡길 수 있는 것!
Spring IoC Container가 관리하는 객체를 Spring Bean 이라 한다. Spring Bean을 등록하는 방법은 기본적으로 @Component Annotation을 통해 이루어진다. Annotation은 Kotlin 및 Java에서 사용되는 메타데이터의 일종으로, @로 시작되며 프로그램 코드에 부가적인 정보를 제공해주는 역할을 한다. 아래와 같이 클래스를 Spring Bean으로 등록할 수 있다.
@Component
class Dependency()
메타데이터 : 시간이 지남에 따라 많은 양의 데이터를 수집, 저장 및 분석할 수 있도록 일관된 방식으로 구조화된, 다른 데이터를 설명하는 데이터이다. 빅 데이터를 데이터 웨어하우스에 저장하여 쉽게 검색하고 관리하려면 메타데이터가 필요하다.
@ComponentScan
은 Spring이 빈을 찾을 때 어느 패키지에서 검색할지를 지정하는데 사용됩니다.@Component
, @Service
, @Repository
, @Controller
등이 붙은 클래스를 스캔하여 빈으로 등록합니다.@Component
는 해당 클래스를 Spring 빈으로 등록하겠다는 것을 표시하는 데 사용됩니다.@Autowired
는 필드, 생성자, 메서드에 사용되며, 해당 타입의 빈을 자동으로 주입하겠다는 의미입니다.@Qualifier
는 여러 빈 중에서 어떤 빈을 주입할지를 명시하는 데 사용됩니다.@Configuration
은 Java 클래스를 Spring의 설정 파일로 지정합니다. 해당 클래스 내부에서 @Bean
어노테이션을 사용하여 빈을 정의할 수 있습니다.@Bean
은 메서드가 반환하는 객체를 Spring 빈으로 등록합니다. 주로 @Configuration
어노테이션이 붙은 클래스에서 사용됩니다.@Service
, @Repository
, @Controller
는 @Component
의 특수한 형태로, 각각 비즈니스 로직, 데이터 액세스 로직, 웹 컨트롤러를 나타냅니다.**@Controller**
와 **@ResponseBody**
가 합쳐진 형태로, 주로 JSON 혹은 XML과 같은 데이터만을 컨트롤러에서 응답할 때 사용합니다.@RequestMapping
은 컨트롤러 메서드에 URL을 매핑하며, 어떤 요청이 들어왔을 때 해당 메서드를 실행할지를 설정합니다.@Configuration 과 @Bean Annotation을 통해 등록할 수 있다.
@Configuration
class DependencyConfiguration() {
@Bean
fun exampleDependency(): Dependency {
return Dependency()
}
}
Bean Scope는 Bean의 생명주기. Bean은 기본적으로 객체기 때문에, Instance가 생성된다. 이 Instance의 생명주기를 결정하는 것이 바로 Bean Scope이다.
Bean scope는 기본적으로 singletone으로 설정된다. 해당 클래스의 인스턴스가 하나만 생성되도록 보장하는 패턴이다. 즉, 스프링에서 기본적으로 Bean은 IoC Containe당 하나만이 생성되고, IoC Container와 생명주기를 같이한다.
싱글톤을 쓰는 이유
큰 이유는 성능 향상과 자원 관리에 있다. Instance를 만들고 초기화 하는 것도 비용이기 때문에, 이미 만들어진 Instance를 재사용한다면 전체적인 성능이 향상된다. 또한, 재사용으로 인해 메모리 자원을 효율적으로 사용할 수 있다.
하나의 Process(보통 프로그램 당 하나씩 배정됨)를 어플리케이션을 구동시키는 공장이라고 하면 Thread들은 컨베이어 벨트라고 할 수 있다. 여러 개의 컨베이어 벨트가 동시에 돌아가서 어플리케이션이 구동된다. 각 벨트에서 하나의 인스턴스를 사용하다 보니 내부의 상태 정보를 가지면 안된다. 이런 상태를 stateless라고 하며, 내부에 상태 정보를 가졌다면 stateful이라고 한다. 예시로, count를 내부의 상태 정보로 가지게 된다면 어떤 쓰레드가 먼저 실행될지 몰라 예측할 수 없기에 오류로 판단 될 수 있다. 그래서 되도록 stateless 방식으로 생성한다.
싱글톤 이외에 다른 생명주기들 또한 개발자가 직접 설정 가능
import org.springframework.stereotype.Component
import org.springframework.context.annotation.Scope
import org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE
@Component
@Scope(SCOPE_PROTOTYPE) => SCOPE_PROTOTYPE 자리에 원하는 scope 설정 가능
class Dependency()
무상태(Stateless) - 클라이언트와 서버 관계에서 서버가 클라이언트의 상태를 보존하지 않음을 의미한다.
장점 : 서버의 확장성이 높기 때문에 대량의 트래픽 발생 시에도 대처를 수월하게 할 수 있다.
단점 : 클라이언트의 요청에 상대적으로 Stateful 보다 더 많은 데이터가 소모된다.
무상태 단어 그대로 생각해보면 "상태가 없는 것" 이다. 그 반대의 경우 "상태가 있는 것" 이라 생각할 수 있으며 이는 상태 유지(stateful) 라 한다. 무상태를 이해하기 위해 우선 상태가 있는 경우를 먼저 살펴봐야 한다.
상태 유지라 함은 클라이언트와 서버 관계에서 서버가 클라이언트의 상태를 보존함을 의미합니다. 단순히 말하면 클라이언트의 이전 요청이 서버에 전달되었을 때, 클라이언트의 다음 요청이 이전 요청과 관계가 이어지는 것을 의미한다.
서버와 클라이언트가 다음과 같이 준비되어있고 클라이언트가 자전거를 사기위해 클라이언트가 서버에게 자전거 부품을 단계별로 커스텀하여 요청을 하는 상황입니다. 서로간의 대화는 다음과 같이 진행.
A : 자전거 사려합니다
X : 자전거 커스텀 재료를 골라주세요 (자전거를 사려한다는 것을 기억한다)
A : 휠은 검정색, 핸들은 검정색, 바디는 흰색, 안장은 흰색으로 해주세요
X : 배송은 어디로 해드릴까요? (자전거를 사려했다는 것을 기억하고 있다)
A : 집으로 보내주세요
X : 결제는 무엇으로 해드릴까요? (자전거를 사려했다는 것, 커스텀을 어떻게 했는지 알고 있다)
A : 카드로 결제할게요
X : 결제 완료 되었습니다. (위의 모든 사용자가 요구했던 사항을 기억하고 있다)
하지만 대량의 트래픽이 몰려들어서 서버를 긴급하게 늘려 판매자 서버 x가 아닌 증가된 어떤 서버 Y라면 문제가 된다. 현실에서도 마트에서 너무 바쁘면 담당자가 바뀌기도 하는 것과 비슷하다.
A : 자전거 사려합니다
X : 자전거 커스텀 재료를 골라주세요
A : 휠은 검정색, 핸들은 검정색, 바디는 흰색, 안장은 흰색으로 해주세요
Y : 네? 어떤걸 말씀하시는거죠?
A : 집으로 보내주세요
Z : ?
A : 카드로 결제할게요
X : 네?
서버가 증설되며 혼합돼 말이 통하지 않게 됐다. 이게 바로 상태 유지의 문제이다. 이러한 문제를 해결하기 위해 무상태가 등장하게된다.
상태 유지를 이해했다면 무상태는 생각보다 쉽게 이해할 수 있다. 바로 무상태의 예를 보도록합시다. 방금 전과 같은 상황을 가정한다.
A : 자전거 사려합니다.
X : 자전거 커스텀 재료를 골라주세요. (서버는 아무것도 기억하지 않는다)
A : 자전거를 사려합니다. 휠은 검정색, 핸들은 검정색, 바디는 흰색, 안장은 흰색으로 해주세요.
Y : 배송은 어디로 해드릴까요? (서버는 아무것도 기억하지 않는다)
A : 자전거를 사려합니다. 휠은 검정색, 핸들은 검정색, 바디는 흰색, 안장은 흰색으로 해주세요. 배송은 집으로 보내주세요.
Z : 결제는 무엇으로 해드릴까요? (서버는 아무것도 기억하지 않는다)
A : 자전거를 사려합니다. 휠은 검정색, 핸들은 검정색, 바디는 흰색, 안장은 흰색으로 해주세요. 배송은 집으로 보내주세요. 결제는 카드로 할게요.
Y : 결제 완료 되었습니다. (서버는 들어온 요청을 처리한다)
무상태의 상황을 잘 보여주는 대화이다. 이전의 클라이언트의 요청(상태)을 유지하지 않는 서버. 그것이 핵심이다. 무상태는 기존의 서버가 혼잡해져서 새로운 서버를 가져다 놓아도 기존의 비즈니스 로직을 그대로 구현하고 있다면 이전의 사용자 요청이 어떤지에 관계없이 계속 일을 처리할 수 있다. 단, 단점은 클라이언트가 하고자하는 최종 목적을 위해 지나는 과정마다 점점 전달해야하는 내용이 많아진다는 것이다.
대표적으로 많이 사용되는 약속 중 HTTP 는 이 무상태를 특징으로 기본적으로 가지고 있다. 특별한 일이 없다면 무상태를 지향해야하며 필요한 경우에만 상태 유지를 해야한다.
Spring은 DI pattern을 적용한 IoC Container(프레임 워크가 제공하는 기능)를 통해 개발자가 작성한 객체들을 Spring Bean으로써 관리하며, Annotation을 붙여줬을 때 이를 관리 대상으로 여긴다는 것!
또한, 기본적으로 Signleton으로 Bean의 Instance를 관리하고, 필요할 시에는 개발자가 직접 Bean의 생명주기를 설정할 수 있다는 것!
이렇게 개발자는 Plumbing을 크게 신경쓸 필요없이, 비즈니스 로직에만 집중할 수 있게 된다!
참고:
https://all-young.tistory.com/17
https://junghyun100.github.io/%ED%9E%99-%EC%8A%A4%ED%83%9D%EC%B0%A8%EC%9D%B4%EC%A0%90/
https://velog.io/@ohzzi/Spring-DIIoC-IoC-DI-%EA%B7%B8%EA%B2%8C-%EB%AD%94%EB%8D%B0
https://roxy.iro.ooo/infra/protocol/http/http-stateful-stateless