파이썬으로 만들어진 무료 오픈소스 웹 애플리케이션 프레임워크(web application framework)
웹 서버 요청이 오면, 장고 urlresolver가 url를 통해 경로를 파악하고, 요청사항을 view(함수 모음)에 전달하면, view가 함수를 통해 db에서 데이터 정보를 찾아, model 정보를 거쳐, template에 렌더링하는 방식으로 처리하는 프레임워크이다.
Django의 프로젝트와 앱 개념
- 프로젝트 : 내가 만들려고 하는 전체 프로젝트 - 앱 : 그 안에 들어가는 각각의 카테고리 또는 상위 기능들 (ex. 회원인증, 메신저 발송)
출처 https://dogfighterkor.tistory.com/4
Django의 MTV(Model, Template, View) 개념
- 모델 : 앱의 데이터와 관련된 부분 (DTO 또는 Entity의 개념) - 템블릿 : 사용자에게 보여지는 부분 (html) - 뷰 : 모델 데이터를 템블릿으로 전달하거나, 템플릿에서 발생하는 이벤트 처리 (요청 및 응답 처리) ㄴ 뷰는 다시 1) 클래스형 뷰, 2) 함수형 뷰로 나뉜다.
python3 --version # 파이썬 버전확인
python3 -m venv myvenv # 나의 가상환경 설정
#Mac
source myvenv/bin/activate # 맥은 bin 폴더를 통해 실행 가능
#Window
.\myvenv\Scripts\activate.bat # 윈도우는 Scripts 폴더를 통해 실행 가능
3. Django 설치
pip install django~={{파이썬 버전}}
Django 프로젝트 시작하기
1. Django 프로젝트 시작
django-admin startproject myweb . # myweb이라는 프로젝트로 시작 끝에 . 안 찍으면 안 됨
2. Django 프로젝트 내 App 설치 및 서버 실행해보기
python manage.py startapp aidiary # aidiary라는 app을 설치
python manage.py runserver # 서버 실행
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('aidiary.urls')),
]
모델 생성하기 - What is migration?
1. 마이그레이션 개념
마이그레이션 : 모델을 데이터베이스에 적용시키는 과정
> makemigrations : 모델의 생성 및 변경 사항에 대한 기록을 파일로 저장하는 것 -- ({{내 앱}}/migrations)에 저장됨 > migrate : makemigrations를 통해 생성된 파일을 실제로 실행하는 것
python manage.py migrate # 마이그레이트를 하겠다 = 모델을 DB에 반영하겠다
초반에 runserver를 하고 나면 이 migrate관련 에러가 뜨는데, 이는 모델에 대한 migrations를 안 해줬다는 경고였어요. 그래서 위 코드를 먼저 작성하면 해결할 수 있었습니다.
2. 어드민 페이지 들어가보기
아래의 코드를 통해서 수퍼 계정을 만들고 121.0.0.1:8000/admin 으로 들어가면 어드민 페이지가 나왔습니다. 거기서 데이터베이스에 대한 관리가 가능해요. 스프링과 다르게 DB 연동을 사전에 안 해도 어드민 페이지를 통해 테스트 과정에서 바로바로 데이터를 입출력할 수 있다는 게 정말 효율적이더군요.
python manage.py createsuperuser # 최상위 어드민 계정 생성
3. 모델 생성하기
App 폴더 하위에 보면 models.py가 있어요. 거기에 모델을 작성해주면 되는데 class 형태로 작성하고, django.db의 models를 사용합니다. Django도 스프링처럼 Orm을 사용해요. 하지만 스프링과 달리, django에서는 entity와 dto에 대해서 명시적인 구분을 따로 하지 않더라고요. (ex. @Table이나 @Entity를 통한 분리) 이 부분은 제가 좀 더 공부하면서 명시적인 구분을 하는 방법이 있는지 보려고 합니다.
이 기능이 진짜 좋은게, migrations 폴더 하위를 보면 변동사항이 생길 때마다 이렇게 무슨 짓(?)을 했는지가 기록으로 남아 있어요. 전체적인 수정사항을 확인할 수 있어서 좋다는 생각을 했습니다.
5. 모델 어드민 페이지에 적용하기
App 폴더 하위에 보면 admin.py 파일이 있어요. 거기에 위처럼 admin.site.register(Diary) 요렇게 생성한 모델을 적용해두면, 어드민 페이지에 Diary에 대한 CRUD 작업을 할 수가 있습니다.
본격적인 작업을 시작하기 전에
1. templates 폴더는 App 폴더 하위에 만들어주기
App 폴더 바로 아래에 templates 폴더를 만들고 그 안에 html 작업을 해주어야 정상적으로 view에서 경로를 찾을 수 있었어요.
2. models.py, urls.py 파일들이 없다면 각각 만들어주기
책에서는 models.py 외에도 forms.py를 따로 만들어주기도 했어요. 저는 처음에 models.py만 사용해보았는데, 상황에 따라 파일을 만들어주시면 될 것 같습니다.
모델 -> 템플릿 -> 뷰 -> Url 순서로 작업하기
책에 나와 있는대로 저는 모델 - 템플릿 - 뷰 - url 순서로 다이어리 프로젝트를 만들어봤어요. 이전에 스프링으로 구현을 해봤어서 따로 설계는 필요 없었기에 여러 사이트에서 디자인 아이디어를 찾아 AI를 활용하여 Html 템플릿을 만들어보았습니다. 현재까지 구현된 바는 아래와 같아요.
[출처 및 참고] 백엔드를 위한 Django Rest Framework with 파이썬 - 권태형 저
- 어플리케이션을 목적에 따라 여러 개의 독립적인 서비스로 분할하여 개발하고 배포하는 방식
MSA의 특징
- 독립된 각 서비스 마다 고유의 ip와 port를 가진다.
- 각각의 서비스가 분산되어 있기 때문에, 배포와 테스트가 어렵다
MSA를 왜 쓸까?
느슨하게 결합된 서비스들의 모임으로 프로젝트를 구성함으로써, 전체 시스템 구조를 논리적으로 구분해 이해하고,
고가용성(부분 장애가 전체 장애로의 확장을 막음)을 높이며, 분산 처리를 가능하도록 하기 위함
MSA를 편리하게 하기 위한 아키텍처 컴포넌트
스프링 클라우드(Spring Cloud)
분산 시스템의 몇가지 공통된 패턴들을 빠르게 빌드할 수 있도록 다양한 툴을 제공해 준다.
예를 들어, Configuration management(설정 관리), Service Discovery(서비스 검색), circuit breakers(회로 차단), intelligent routing(라우팅), micro-proxy, control bus, short lived microservices and contract testing.
Consumer-driven and producer-driven contract testing
Service Discory란? 왜 쓸까?
클라언트에게 필요한 서비스가 어디에 있는지 빠르게 찾으려고.
서비스를 등록/해제/조회할 수 있는 API를 제공하며, 고가용성이 보장되어야 한다.
Service Registry를 통해 서비스를 관리하며, 지속적인 서비스 상태 모니터링이 가능하다.
- Netflix Eureka, Apache Zookepper 등이 있음
Eureka
Spring Cloud Netflix는 Netflix OSS와 Spring 어플리케이션을 통합해주는 것으로, autoconfiguration 및 Spring 환경과 다른 Spring 프로그래밍 모델과의 binding을 제공합니다. 몇 가지 간단한 Annotation을 사용하면 애플리케이션 내부의 공통 패턴을 신속하게 활성화 및 구성하고, 검증된 Netflix 구성 요소를 사용하여 대규모 분산 시스템을 구축할 수 있습니다. 제공되는 패턴에는 Service Discovery(Eureka)가 포함됩니다. (출처:spring cloud netflix 공식 document)
-> 다시 말해, Netflix OSS를 Spring에서 사용할 수 있도록 해주는 것으로,
-> Netflix는 2007년 DB 문제로 서비스가 중단된 이래 신뢰성이 높고 scale-out한 시스템을 구축하기 위해 Netflix OSS를 만들었다.
-> Netflix OSS는 1) 고가용성(하나가 망해도 다른 하나가 산다), 2) scale-out, 3) easy deploy의 특징을 갖는다.
-> Eureka 또한 Netlfix OSS의 하나로, Service Discovery를 담당하는 모듈을 말한다.
Service Discovery: Eureka instances can be registered and clients can discover the instances using Spring-managed beans
Service Discovery: an embedded Eureka server can be created with declarative Java configuration
Eureka Service Discovery의 용어
- Discovery : 검색
- Registry : 등록 목록
- Eureka Client : 각 서비스에 해당되는 모듈
ㄴ 자기 자신을 Eureka Server에 등록한다.
ㄴ Eureka Server를 통해 다른 Client의 정보를 검색할 수 있다.
- Eureka Server : Eureka Client를 관리하는 서버
ㄴ Eureka Client 정보를 Registry에 등록
ㄴ Heartbeat를 통해 Client가 수행 중임을 확인
Eurkea Service Discovery 흐름의 예시
1. Eureka Client 서비스가 시작될 때, Eureka Server에 자신의 정보를 등록
2. Eureka Client는 Eureka Server로부터 다른 Client의 연결 정보가 등록되어 있는 Registry를 받고 Local에 저장한다.
3. N초마다 Eureka Server로부터 변경 사항을 갱신받는다.
4. N초마다 ping를 통해 자신이 동작하고 있다는 신호를 보낸다. 신호를 보내지 못하면 Eureka Server가 보내지 못한 Client를 Registry에서 제외시킨다
Eureka Service Discovery 사용하기
- Eureka Server
# eureka server port
server:
port: 8761
# MSA module id (eureka)
spring:
application:
name: mindchat
# Eureka server does not need to be included
eureka:
client:
register-with-eureka: false # 레지스트리에 자신을 등록할 것인가 (default : true)
fetch-registry: false # 레지스트리에 있는 정보를 가져올 것인가
service-url: #server의 위치 지정
defaultZone: http://localhost:8761/eureka
- Eureka Client
ㄴ eureka.client.register-with-eureka : 레지스트리에 자신을 등록할 것인가 여부
ㄴ eureka.client.fetch-registry : 레지스트리에 있는 정보를 검색할 것인가 여부
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
accept-count: 100 # 작업 큐의 사이즈
다수의 요청이 존재할 때 Spring MVC는 Queue안에 요청 사항을 받아두고, 미리 지정해둔 Thread수에 따라, Thread Pool의 Thread를 사용하여 멀티태스킹을 하게 된다. 이 때, 각각의 Thread는 동기적인 처리를 하게 되는데, 하나의 요청을 받게 되면 응답까지 한 번에 받을 수 있도록 처리하며, 하나의 함수에 대해 return을 받을 때까지 블로킹 처리를 한다.
- 동기적이다 : 호출과 응답이 동시에 이루어진다. (ex. 공을 던지고 받는다) - 비동기적이다 : 호출과 응답이 동시에 이루어지지 않는다. (ex. 기차에 공을 올려 도착할 때까지 기다린다) - 블로킹 : 함수를 콜했을 때 응답을 받기 위해 멈춰있는 상태 - 논블로킹 : 함수를 콜했을 때 응답을 받기 위해 멈춰있기 않고 다음 줄을 실행하는 상태
이 방식의 경우, 정해진 time-slice에 따라 Queue로 이동하며 멀티 태스킹을 하게 되는데, 다수의 Client의 요청에 대하여 DB데이터 응답 시간이 지연될 경우, 해당 시간 동안 손가락을 빨고 기다리는 상황이 발생할 수 있게 된다.
이를 해결하기 위한 방법이 Spring Webflux이다. Spring Webflux는 Node.js와 같이 Event Loop가 돈다. JavaScript는 비동기로 동작을 하는데, 함수를 Stack에 두어 LIFO 방식으로 실행하는 것은 Java와 동일하지만, 함수를 Callback Queue에 또한 넣어둠으로써 Stack이 다 비워졌을 때 FIFO으로 Queue의 함수를 실행하도록 Event Loop를 돈다.
Javascript Event Loop
Webflux 또한 이러한 Event Loop를 사용한다. 10초의 지연 시간이 있는 요청이 있다면 해당 요청에 대한 핸들러(=함수)에게 처리를 위임하고, 처리가 완료되는 즉시 Callback Queue에 Callback을 추가한뒤 Stack을 통해 실행하게 된다. 10초의 지연 시간 동안 처리할 다른 함수가 있다면 해당 함수가 Stack에 두어 실행된다. 이러한 처리 방식을 비동기/논블로킹 처리 방식이라 하며, 이벤트 발생에 대해 반응을 한다하여, Reactive Programming이라고 한다.
이 많은 app들의 endpoint(ip+host+port+path)를 일일히 관리하려면 엄청난 수작업이 필요하고,
각각의 서비스들마다 공통적으로 들어가는 인증/인가, 로깅 등의 기능을 중복으로 작업하는 것에 어려움이 생긴다.
[1] 공통 로직 처리
Gateway를 일종의 Proxy(대리자)로 두게 되면, 각각의 요청사항에 대한 공통 기능(인증/인가 및 로깅)을 편리하게 작업할 수 있다.
[2] Api Routing
라우팅은 네트워크 상에서 경로를 선택하는 프로세스를 의미한다. API Routing이란 동일한 API 요청들에 대해,
각각의 클라이언트나 서비스에 따라 다른 엔드포인트를 사용할 수 있도록 해주는 것을 말한다.
(1) 로드밸런싱
API Gateway를 사용하면 여러 개의 서비스 서버로 API를 분산시켜 주기 때문에 로드밸런서 역할을 한다.
(2) 클라이언트 및 서비스별 엔드포인트 제공
API Gateway를 사용하면 요청을 공통 API로 보내더라도 각 클라이언/서비스별로 엔드포인트를 분산할 수 있다.
(3) 메세지/헤더 기반 라우팅
메세지나 헤더 기반으로 API 라우팅을 할 수 있다. 라우팅에서 요청 API에 대한 메세지를 파싱할 때 많은 파워를 소모할 수 있기 때문에, Restful API를 제공할 경우, Header에 Routing 정보를 두어 Header정보를 파싱하도록하고, Body는 포워딩 할 경우 API Gateway의 부하를 줄여줄 수 있다.
[3] Mediation 기능
(1) 메세지 포맷 변환
클라이언트와 서버가 서로 다른 메세지 포맷을 사용할 때, 이를 적절히 변환시켜 주는 역할을 의미
(2) 프로토콜 변환
내부 API는 gRPC를 사용하고 외부 API는 Restful API를 사용함으로써 내부 API의 성능을 높이면서 범용성을 높일 수 있도록 프로토콜 변환이 가능하다.
(3) Aggregation
서로 다른 API를 묶어서 하나의 API로 제공하는 것을 의미한다. 어떤 요청에 대해 A/B/C모듈 각각의 API를 호출해야할 경우, API Gateway를 사용하면 A/B/C API를 한번에 처리가 가능하다.
Gradle에서 의존성 추가
** 주의 사항1
spring-cloud-starter-gateway-mvc : Spring Mvc 기반 / Servlet API / Blocking I/O 모델 사용 / Tomcat
spring-cloud-starter-gateway : Spring Webflux 기반 / Reative API / Non-blocking I/O 모델 사용 / Netty
** 주의 사항2
spring-cloud-starter-gateway는 Webflux 기반이기 때문에 SpringMVC기반의 라이브러리와 함께 쓸 경우,
"Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway." 메세지가 발생한다.
따라서 spring-boot-starter-web 등의 MVC 기반 라이브러리는 같이 사용하면 안된다.
- Service Discovery는 말 그대로 서비스를 찾아주는 것으로써, 어느 위치에 어떤 서버가 있는지를 찾아준다.
rf. key / value 형태로 서비스를 등록하고 검색할 수 있도록 해준다.
- 넷플릭스 자사의 기술들을 Java Spring 재단에 기부를 하면서 만들어진 것이 Eureka이다.
- Eureka를 쓰려면 먼저 서비스를 등록해야 한다.
- Client에서 요청이 Load Banlancer로 들어오면 Service Discovery가 서비스의 위치를 찾아준다.
2. 프로젝트 환경 구성
[1] 프로젝트 생성
- 마인드챗이라고, 내 마음 속 이야기를 채팅창에 적을 수 있는 플젝을 한 번 만들어보고 싶어서 아래처럼 만들어봤다.
[2] 의존성에서 유레카 서버 하나만 먼저 넣어준다.
- build.gradle의 모습
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
[3] 메인 클래스에 유레카 서버를 활성화하는 Annotation 추가
@SpringBootApplication
@EnableEurekaServer
public class MindchatApplication {
public static void main(String[] args) {
SpringApplication.run(MindchatApplication.class, args);
}
}
[4] application.yml 작성
# service port (Eureka 서버 포트)
server:
port: 8761
# MSA 각 서비스의 고유 id 부여
spring:
application:
name: discoveryservice
# 마이크로 서비스를 등록하는 역할을 한다.
# Eureka는 서버로서 구동만 하면 되기 때문에 아래 사항을 false로 입력
eureka:
client:
register-with-eureka: false
fetch-registry: false
- 실행 시, 로그에 8761 포트 뜸
main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8761 (http)
- http://localhost:8761 로 들어오면 Eureka Dashboard 화면 보임
피보탈에서 아래에 +3 (Api First, Telemetry(수치화/시각화), Authentication and authorization) 을 더 추가했다.
I. Codebase
One codebase tracked in revision control, many deploys
> 리포지토리에 저장한 각 마이크로 서비스에 대한 단일 코드베이스 / 버전 제어 위함 / 코드는 한 곳에서 배포
II. Dependencies
Explicitly declare and isolate dependencies
> 종속성. 각 마이크로 서비스는 각각의 모듈에 종속적이라 전체 서비스에 영향을 주지 않음
III. Config
Store config in the environment
> 설정
IV. Backing services
Treat backing services as attached resources
> 서비스 지원. 데이터/캐싱/메시징 등을 지원
V. Build, release, run
Strictly separate build and run stages
> 빌드/릴리즈/실행환경을 분리하라. 각 태그 있고 분리되며 자동화된 배포가 이뤄져야한다.
VI. Processes
Execute the app as one or more stateless processes
> 하나의 프로세스는 다른 프로세스와 독립되어야 한다.
VII. Port binding
Export services via port binding
> 포트 바인딩. 각 서비스별로 포트가 달라야한다.
VIII. Concurrency
Scale out via the process model
> 동시성. 하나의 서비스가 여러 인스턴스에 나뉘며 부하 분산 -> 동시성을 가져야 한다.
IX. Disposability
Maximize robustness with fast startup and graceful shutdown
> 확장성. 서비스 인스턴스 등록 및 삭제 / 실행이 쉬워야
X. Dev/prod parity
Keep development, staging, and production as similar as possible
> 개발/운영 분리
XI. Logs
Treat logs as event streams
> 로그를 출력하는 로직을 어플리케이션과 분리해야 한다. (별도의 모니터링)
> Azure 또는 ELK를 사용할 수 있음
XII. Admin processes
Run admin/management tasks as one-off processes
> 적절한 관리. 마이크로 서비스가 어떠한 상태로 사용되며 리소스가 어떻게 쓰이는지
데이터를 정리하고 분석하는 기능을 통해 관리가 가능하다.
5. 모놀리틱 vs Microservice
모놀리틱은 하나에 모든 서비스를 다 넣는다면, MSA는 서비스를 분리해서 개발하고 운영하는 걸 말한다.
C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java:33: warning: @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final.
private List<ProductItem> productItems = new ArrayList();
^
Note: C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
- 테스트 코드
@Test
void addProduct() {
// given
Long sellerId = 1L;
AddProductForm form = makeProductForm("나이키 에어포스", "신발", 3);
// when
Product p = productService.addProduct(sellerId, form);
// then
Product result = productRepository.findById(p.getId()).get();
Assertions.assertNotNull(result);
Assertions.assertEquals(result.getSellerId(), 1L);
Assertions.assertEquals(result.getName(), "나이키 에어포스");
Assertions.assertEquals(result.getDescription(), "신발");
Assertions.assertEquals(result.getProductItems().get(0).getName(), "나이키 에어포스0");
Assertions.assertEquals(result.getProductItems().get(0).getPrice(), 10000);
}
private static AddProductForm makeProductForm(String name, String description, int itemCount) {
List<AddProductItemForm> addProductItemForms = new ArrayList<>();
for (int i = 0; i < itemCount; i++) {
addProductItemForms.add(makeProductItemForm(null, name + i));
}
return AddProductForm.builder()
.name(name)
.description(description)
.addProductItemForms(addProductItemForms)
.build();
}
> 원인 :
- @OneToMany의 default fetch type이 LazyLoading이기 때문에, Proxy로 id 값만 담은 ProductItem들을 가져오고, 실제 내용은 들어있지 않았기 때문이다.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long sellerId;
private String name;
private String description;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "product_id")
private List<ProductItem> productItems = new ArrayList();
public static Product of(Long sellerId, AddProductForm form) {
return Product.builder()
.sellerId(sellerId)
.name(form.getName())
.description(form.getDescription())
.productItems(form.getAddProductItemForms().stream()
.map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
).build();
}
}
> 해결 :
이를 해결하기 위해서는 두가지 방법을 사용할 수 있는데
(1) fetch type을 EAGER로 변경한다. --> 이 방법은 그러나 불필요하게 매번 DB 조회 시 모든 데이터를 한 번에 가져오게 함으로 좋지 않다.
(2) JPA의 @EntityGraph와 findWith 함수를 통해 속성을 지정(ex. productItems)할 때, fetch type을 변경시킨다.
EntityGraphType.LOAD
attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 default fetch type으로
EntityGraphType.FETCH
attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 LAZY로
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@EntityGraph(attributePaths = {"productItems"}, type = EntityGraph.EntityGraphType.LOAD)
Optional<Product> findWithProductItemsById(Long id);
}
* persist() 는 리턴값이 없는 insert, merge() 는 리턴값이 없는 update
@Transactional
public <S extends T> S save(S entity) {
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
* save() 는 리턴값이 있는 insert, update이다.
| 예시 코드
- Product (상품) 하위에 옵션으로 들어가는 Item들이 있다고 할 때,
- Product의 List<ProductItem> productItems에 @OneToMany(cascade = CascadeType.ALL)을 달아주어, 상품에 대한 CRUD가 이루어질 때 하위 엔터티인 productItems의 ProductItem까지 영향을 받는다.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long sellerId;
private String name;
private String description;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "product_id")
private List<ProductItem> productItems = new ArrayList();
public static Product of(Long sellerId, AddProductForm form) {
return Product.builder()
.sellerId(sellerId)
.name(form.getName())
.description(form.getDescription())
.productItems(form.getAddProductItemForms().stream()
.map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
).build();
}
}
-- MySQL 기준
create table member(
email varchar(50),
name varchar(255),
unique key member_uk_email (email, name)
)
- 중복 데이터 저장을 막기 위한 방법으로 아래 두가지를 사용할 수 있다.
* 단순 insert into 문의 경우 에러를 발생하지만, 아래 두 가지는 에러를 발생시키지 않는다.
INSERT IGNORE
unique key가 걸린 칼럼에 중복 데이터가 이미 있는 경우 삽입 X
INSERT ~~~ ON DUPLICATE KEY UPDATE
unique key가 걸린 칼럼에 중복 데이터가 이미 있는 경우 업데이트
insert ignore into member values ('jamie@gmail.com','JAMIE');
-- 0 raw affected (error x)
insert ignore into member values ('jamie@gmail.com','JAMIE');
insert ignore into member values ('jamie@gmail.com','JAMIE');
insert ignore into member values
on duplicate key update ('jamie@gmail.com','JAMIE');
-- 2 raw affected (error x)
insert ignore into member values
on duplicate key update ('jamie@gmail.com','JAMIE');
insert ignore into member values
on duplicate key update ('jamie@gmail.com','JAMIE');
(2) Index : 추가적인 쓰기 작업과 저장공간을 활용해 DB table의 검색 속도를 향상시키기 위한 자료구조
- Index는 다양한 자료구조를 통해 만들 수 있다.(ex. Hashtable)
- 가장 많이 사용되는 자료구조는,B+Tree 구조이다.
* B+Tree 구조란, DB인덱스를 위해 자식노드가 2개 이상인 B-Tree를 개선시킨 자료구조를 말한다.
* 일반적으로 조회시 O(logN)의 시간을 가진다.
- Index는 아래처럼 두 가지로 종류를 나눌 수 있다.
cluster index
primary key를 통해 인덱스를 설정(ex. 처음부터 정렬된 영어사전)
보조 index
unique key를 통해 인덱스를 설정(ex. 책 뒤의 찾아보기)
* 참고 )
cluster와 보조index를 같이 쓸 경우 INDEX로 입력할 수 있지만, 보조 index만 쓸 경우 UNIQUE INDEX를 써야한다.
- Index는 아래와 같이 테이블을 생성할 때 만들 수도 있고, 별도로 alter문으로 제약조건을 붙일 수도 있다.
create table member(
id bigint primary key, -- cluster key
email varchar(50),
name varchar(255),
unique key member_uk_email (email, name),
INDEX member_idx (id, email)
)
- Index의 장단점
장점
- Index를 사용하면 조회 속도를 향상시킬 수 있어, DB 서버의 부하를 줄일 수 있다.
단점
- 하지만 추가 저장공간을 사용해야하고, - insert, update, delete 같은 데이터 변경 쿼리가 자주 사용되는 경우에 인덱스를 쓰면 paging이 빈번해져 성능이 악화될 수 있다.(== 조회 보다 db 변동이 많은 경우 불리) - 추가적으로 cardinality가 낮은 경우(= 중복 데이터가 많은 경우), 인덱스를 사용하는 것이 비효율적일 수 있다.
* Selectivility :데이터 집합에서 특정 값을 얼마나 잘 골라낼 수 있는지에 대한 지표
>> Selectivility = Cardinality / Total number of Records
>> Selectivility = 1 <-- 모든 레코드가 유니크하다.
* cardinality :특정 데이터 집합의 유니크(Unique)한 레코드의 개수
- Index를 설계할 때 알아둘 점
[효율적인 인덱스 설계 ] - Where절에 사용되는 열 - Select절에 자주 등장하는 칼럼을 잘 조합해 Index로 만들면 조회 시간을 줄일 수 있다. - JOIN절에 자주 사용되는 열 - Order by 절에 사용되는 열은 클러스터 인덱스가 유리하다.
[금지해야할 인덱스 설계] - 대용량 데이터가 자주 입력되는 경우 primary보다 unique를 설정한다. - 데이터 중복도가 높은 열은 인덱스 효과가 없다. (ex. 성별) - 자주 사용되지 않으면 성능 저하를 초래할 수 있다.
| 스프링에서 중복 데이터 저장을 방지하기
1. Primary key 설정하기
- 사용할 칼럼에 @Id 를 넣어준다. - @GeneratedValue 를 통해 auto_increment가 가능하게 한다.
* auto : 자동선택, identity : db identity 칼럼 사용, sequence : 시퀀스를 쓰는 db vendor에서 사용
2. Unique Key & Index 설정
- @Entity 클래스에서, @Table(uniqueConstraints = {}) 를 통해 unique 제약조건을 설정한다.
- Unique key를 아래와 같이 설정하게 되면 해당 키를 곧 인덱스로 인식한다.
@Entity(name = "DIVIDEND")
@NoArgsConstructor
@Getter
@ToString
@Table( // 복합 unique키 설정 (중복 저장 X)
uniqueConstraints = {
@UniqueConstraint(
columnNames = {"companyId", "date"}
)
}
)
public class DividendEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long companyId;
private LocalDateTime date;
private String dividend;
public DividendEntity(Long companyId, Dividend dividend) {
this.companyId = companyId;
this.date = dividend.getDate();
this.dividend = dividend.getDividend();
}
}
/* 이메일 인증 시 서버 주소 필요하여 별도로 서버 설정 */
/* rf. https://www.baeldung.com/spring-boot-configuration-metadata */
@Configuration
@ConfigurationProperties(prefix = "server")
@Getter
@Setter
public class ServerPropertyConfig {
private String name;
private String ip = "127.0.0.1";
private int port = 8080;
private String address = String.format("http://%s:%d", ip, port);
}
3. Form 데이터를 Put/Delete로 전송할 때 Get으로 자동 변환 막기
- 이건.. 음 정확한 정보를 더 찾아봐야할 것 같다.
- 일단 알게 된 사항은 아래와 같이 hiddenmethod를 설정하면,
<input type = hidden> 을 통해 전달 받은 데이터를 PUT/DELETE로 처리할 수 있다는 것 정도이다.
mvc:
hiddenmethod:
filter:
enabled: true
4. H2 DB의 오류
[ 상황 ]
- ddl.sql과 schema.sql이 안 먹히고 datasource관련 exception이 터짐
[ 원인 ]
- 아래와 같이 schema.sql에 user를 적어두었었는데,
- H2 db의 경우 user는 내장된 예약어인지 어쩐지 어쨌든 사용하지 못하게 되어 있다고 한다.
drop table if exists user -- error
[ 해결 방법 ]
- cascade를 붙여서 작성한다.
drop table if exists "user" cascade
create table "user"
(
)
[ 그러나... ]
- 이 방법을 하게 되면 1.에서 development와 production에 따라 db vendor를 다르게 설정한게 의미가 없어진다.
- 왜냐고? production에서 나는 mysql를 쓸 건데, mysql은 또 "user"를 인식못한다. (이런..으귬느앎ㄴ;ㅣ러ㅏㅐㅂ쟛)
- 서비스에서 @Transactional 을 사용할 때와 사용하지 않을 때 수정과 삭제 시 차이를 보이는 걸 경험했다.
@Transactional
update
delete
사용
repository.save() 불필요
repository.delete() 시 삭제됨
미사용
repository.save() 필요
repository.delete() 시 삭제되지 않음
- 더불어 테스트에서 아래와 같이 @Transactional을 작성할 때, DB에 영구반영이 되지 않는 걸 확인했었다.
// 아래는 TEST 코드
@SpringBootTest
@Transactional
public class JpaMemoRepositoryTest{
// 테스트 작성
}
- 왜 그럴까?
(1) 수정 시 save하지 않아도 수정되는 이유
: @Transactional이 붙여진 메소드는 spring boot에서 트랜잭션으로 인식된다. 해당 메소드에서 DB의 row를 조회한 후, setter로 row에 변화를 준다고 하자. Hibernate는 persistent entities의 변동사항을 자동적으로 확인하고, 그에 따라 DB에 업데이트를 해준다.
: 이 부분은 정확하게는 모르겠지만, JPA의 PersistenceContext와 연관되어 있다고 한다. @Transactional을 명시하지 않을 경우, 해당 메소드는 트랜잭션으로 인식되지 않는다. 이 부분은 좀 더 알아보아야 할 것 같은데 save와 달리 delete는 삭제되지 않는다는 점만 일단 기록해두려 한다.
(3) 테스트에서 @Transactional을 쓸 경우
: 스프링부트의 test로 지정된 패키지 내부의 클래스에 @Transactional을 쓰게 되면 모든 트랜잭션이 롤백되도록 되어 있다.
따라서, 테스트에서 @Transactional을 쓸 경우 DB에 변동사항이 반영되지 않는다. 그럼에도 쓰는 이유는 서비스 로직을 파악하기 간편하기 때문. 검색해보면 일부 일부 사람들은 테스트에서는 해당 어노테이션을 쓰지 말 걸 권장하기도 하는데, 케바케인 것 같다.
(4) @Transactional와 연관해서 지연로딩과 즉시로딩에 대한 오류에 대해서도 알아볼 필요가 있다.
Lazy Loading
가능한 객체의 초기화를 지연시킨다. (참조하는 엔터티를 당장 쓰지 않을 경우)
Eager
객체의 초기화를 지금 당장 한다. (참조하는 엔터티를 지금 쓰는 경우)
- A엔터티 내부에 @OneToMany 라는 Annotation으로 참조하는 B엔터티 데이터가 있다고 할 때, @OneToMany의 fetch type은 디폴트로 Lazy로 되어있다.
- 이는 지연 로딩을 의미하는데, 지연 로딩의 경우, 참조된 B엔터티의 데이터들을 모두 읽지 않고 Proxy 객체(id값만 있음)를 통해 가져온다. 이 경우, 아래와 같이 코드를 작성하게 되면 오류를 발생시킨다.
@RestController
class Controller{
@GetMapping
void findOneUserAndPetInfo(long userId){
User user = userService.findByUserId(userId);
List<Pet> pets = user.getPet(); // error!!
}
}
- 이를 해결하는 방법은 서비스 메소드에 @Transactional을 붙이는 것이다. 그렇게 하면, Lazy Loading을 하더라도, 해당 메소드를 트랜잭션으로 인식해 필요한 데이터를 적시에 초기화한다.
- DEFAULT : DATABASE의 기본 격리성 레벨 - READ_UNCOMMITED : 트랜잭션 commit 전 다른 트랜잭션 조회 가능 (Dirty Read 발생) - READ_COMMITED : 트랜잭션 commit 후 다른 트랜잭션 조회 가능 (Dirty Read 방지) - REPEATABLE_READ : 트랜잭션이 조회중인 특정 Row에 대해 shared lock을 걸어, 다른 트랜잭션이 해당 Row을 조회하지 못하도록 하는 것 (Non-repeatable Read 방지) - SERIALIZABLE : 트랜잭션이 사용하는 모든 데이터에 대해 shared lock을 걸어, 다른 트랜잭션의 개입을 완전 봉쇄하는 것 (Phantom read 방지)
// 현장에선 REPEATABLE_READ을 주로 사용한다고 한다. @Transactional(isolation = Isolation.REPEATABLE_READ)
Propagation(전파수준)
트랜잭션 동작 중 다른 트랜잭션을 호출하는 상황에서, 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값
* 트랜잭션 A 안에 트랜잭션 B를 호출하는 경우, 트랜잭션 A는 부모 트랜잭션 , B는 자식 트랜잭션이 된다. * 참여한다 = 부모 트랜잭션의 흐름 안에 포함되어 동작한다.
- REQUIRED : 디폴트 속성. : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션 뒤에 붙는다. : 새로운 트랜잭션을 생성한다. : 예외가 발생하면 롤백되며 호출한 곳에도 롤백이 전파된다. - SUPPORT : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션을 계속 진행한다. : 실행 중인 트랜잭션(부모)이 없으면, 트랜잭션이 아닌 일반 로직으로써 동작한다.
- REQUIRES_NEW : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션은 대기하고, 새 트랜잭션을 만든다. : 예외가 발생해도 호출한 곳에 롤백이 전파되지 않는다.
- NESTED : = 중첩 트랜잭션. : 실행 중인 트랜잭션(부모)이 있으면, save point를 지정하고 새로운 트랜잭션을 생성한다. : 만약 자식 트랜잭션에서 예외가 발생할 경우, save point까지 롤백된다. : 만약 실행 중인 트랜잭션(부모)이 없을 경우, REQUIRED와 동일하게 작동한다.
@Transaction(rollbackFor=Exception.class) : 예외 상황 발생 시 롤백된다 @Transaction(noRollBackFor=Exception.class) : 어떤 예외가 되더라도 커밋된다
* 디폴트 : RuntimeException, Error
timeout
일정 시간 내에 트랜잭션 끝내지 못하면 롤백
@Transaction(timeout=10)
| 정리
- 트랜잭션의 개념
(1) 트랜잭션은, DB의 상태를 변화시키는 하나의 작업 단위, 다시 말해 CRUD 행위를 말한다.
(2) 트랜잭션은, 커밋과 롤백이라는 두 가지 연산을 통해, 총 5가지의 상태 (실행중, 일부 커밋, 완전 커밋, 실패, 롤백) 를 가질 수 있다.
(3) 트랜잭션의 특징으로는 ACID가 있는데,
- 원자성은 All or Nothing을 말하며, - 일관성은 작업 처리 결과는 늘 일관적이어야 한다는 의미이며, - 독립성은 한 트랜잭션이 실행되고 다른 트랜잭션이 수행되어야 한다는 의미이며, - 지속성은 한 번 변화를 시켰으면 그것이 영구적으로 반영되어야 한다는 걸 의미한다.