MSA

- 어플리케이션을 목적에 따라 여러 개의 독립적인 서비스로 분할하여 개발하고 배포하는 방식  

 

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.

- 공식 도큐먼트 : https://spring.io/projects/spring-cloud

 

스프링 클라우드 피처

  • Distributed/versioned configuration
  • Service registration and discovery
  • Routing
  • Service-to-service calls
  • Load balancing
  • Circuit Breakers
  • Distributed messaging
  • Short lived microservices (tasks)
  • 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를 담당하는 모듈을 말한다.

* 자세한 내용은 다음 링크에 들어가 있다. https://p-bear.tistory.com/77

 

Eurkea의 피처

  • 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:
  port: 0 #랜덤 포트 사용

spring:
  application:
    name: user-service

eureka:
  instance:
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka

 

port를 0으로 설정하면 랜덤 포트가 지정된다. 이렇게 되면 동일 서비스를 여러 개의 랜덤 포트로 빌드 후 실행이 가능하다.

(ex. user-service 앱이 port:53000과 port:533333에서 실행되고 있다)

그러나 유레카는 각각의 서비스를 프로젝트 내에서 지정한 ip/port를 통해 구분하기 때문에, http://localhost:8761에 접속하여 실행중인 application을 보면 1개의 app만 실행 중인 것으로 나타난다.

(Eureka에서 0번 포트의 앱이 두개 있더라고 하나로 치부) 

이때, eureka.instance.instance-id를 사용하면 eureka에서 app을 구분하는 값을 지정된 값으로 변경할 수 있다.

 

Spring Gateway

Spring MVC는 동기적이며 논블로킹 처리를 하는 Servlet 기반의 프레임워크이다.

출처:https://velog.io/@tritny6516/Spring-Thread-Pool

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이라고 한다.

 

Spring MVC Webflux
동기 비동기
블로킹 논블로킹
명령형 프로그래밍 반응형 프로그래밍
tomcat netty
JDBC,JPA 네트워킹 지원 반응형 라이브러리(Reactor, RxJava) 지원

출처 : https://pearlluck.tistory.com/726

 

Gateway는 왜 사용해야 하나?

기본적으로 작은 서비스들당 50~100개의 app들이 돈다고 할 때,

이 많은 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 기반 라이브러리는 같이 사용하면 안된다.

dependencies {
   implementation 'org.springframework.cloud:spring-cloud-starter-gateway' #추가
   implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
   compileOnly 'org.projectlombok:lombok'
   annotationProcessor 'org.projectlombok:lombok'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
server:
  port: 8000

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      default-zone: http://localhost:8761/eureka

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service #서비스id
          uri: http://localhost:8081/
          predicates: # 조건 
            - Path=/first-service/**
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**

Netty가 작동했다.

위 상태로만 gateway를 넣게 되면 /**까지 같이 엔드포인트에 포함되기 때문에 필터를 통한 작업이 추가적으로 필요하다.

 

Spring Cloud Gateway에 Filter 적용하기

출처:인프런_springcloud로 개발하는 마이크로서비스애플리케이션

[1] Java 코드 

@Configuration
public class FilterConfig {

    @Bean
    public RouteLocator gateRoutes(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/first-service/**")
                        .filters(f -> f.addRequestHeader("first-request", "first-request-header")
                                .addResponseHeader("first-response", "first-request-response"))
                        .uri("http://localhost:8081/")
                )
                .route(r -> r.path("/second-service/**")
                        .filters(f -> f.addRequestHeader("second-request", "second-request-header")
                                .addResponseHeader("second-response", "second-request-response"))
                        .uri("http://localhost:8082/")
                )
                .build();
    }

}

[2] property(yml)

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header2 # key, value
            - AddResponseHeader=first-response, first-response-header2
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header2 # key, value
            - AddResponseHeader=second-response, second-response-header2

 

Custom Filter 사용하기

@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {

    public CustomFilter(){
        super(Config.class);
    }

    public static class Config{
        // configuration properties
    }

    @Override
    public GatewayFilter apply(Config config) {
        // Custom Pre Filter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Custom PRE filter : request id -> {}", request.getId());

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {  // Mono : 비동기 방식에서의 단일 응답값 의미
                log.info("Custom POST filter : response code -> {}", response.getStatusCode());
            }));
        };
    }
}

 

GlobalFilter 사용하기

- 모든 라우터에서 전역적으로 필터를 사용할 수 있다.

- GlobalFilter를 만들고, application-yml에 default-filters로 해당 필터를 입력해주면 전역 필터로 동작한다.

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {

    public GlobalFilter(){
        super(Config.class);
    }

    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Global Filter BaseMessage : {}", config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info("Global Filter Start : request id -> {}", request.getId());
            }

            // Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPreLogger()) {
                    log.info("Global Filter End : response code -> {}", response.getStatusCode());
                }
            }));
        };
    }
}
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true

 

Load Balancer

기존의 경우 uri에 직접 ip:port를 전달함으로써 gateway에서 해당 ip:port로 이동했다면

gateway에 아래와 같은 형식으로 uri에 넣어줌으로써, Eureka를 거쳐 ip:port로의 이동을 할 수 있다.

- lb://{서비스 어플리케이션 name의 upper}

routes:
  - id: user-service
    #uri: http://localhost:8081/
    uri: lb://USER-SERVICE
    predicates:
      - Path=/user/**
    filters:
      - name: CustomFilter
      - name: UserLoggingFilter
        args:
          baseMessage: Spring Cloud Gateway User Logger Filter
          preLogger: true
          postLogger: true

 

[ 참고/출처 ] 

https://joon2974.tistory.com/26

https://kinchi22.github.io/2019/09/22/api-gateway/

https://velog.io/@tritny6516/Spring-Thread-Pool

https://velog.io/@seokkitdo/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EB%9E%80

https://pearlluck.tistory.com/726

https://p-bear.tistory.com/77

https://adjh54.tistory.com/207

https://djlife.tistory.com/10#google_vignette
https://velog.io/@ksh9409255/Service-Discovery

https://velog.io/@jkijki12/Eureka%EB%9E%80

 

 

1. Spring Cloud Netflix Eureka 

- 유레카는 Service Discovery 중 하나이다.

- 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. 마이크로 서비스 모듈 만들어보기 

- [File] - [new project] 로 모듈을 만든다.

- 중요한 건 이번에는 Eureka Discovery Client 를 추가한다.

- 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'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

ext {
	set('springCloudVersion', "2023.0.0")
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}

tasks.named('test') {
	useJUnitPlatform()
}

 

- Main 클래스에 아래와 같이 어노테이션 추가

@SpringBootApplication
@EnableDiscoveryClient
public class UserApplication {
	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}

}

- yml 설정

server:
  port: 9001

spring:
  application:
    name: userservice

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka

- 유레카 확인

 

[ 출처 ] 인프런 - Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)

[ 참고 ]

https://bcho.tistory.com/1252

 

시간이 지날 수록 소프트웨어는 Antifragile한 아키텍처를 구성하고자 하였으며, Cloud Native한 아키텍처를 구성하고자 했다. 아래는 그러한 소프트웨어 아키텍처의 특징을 정리한 내용이다.

 

1. Antifragile 아키텍처

1) AutoScaling (자동확장성)

   : 상황에 따라 인스턴스의 갯수를 자동으로 늘리는 등 자동 확장이 가능한 것

2) Microservices

    : 세밀한 단위로 모듈과 기능을 세분화한 서비스. 넷플릭스와 아마존에서 가장 잘 구축했음 

3) chaos engineering

   : chaos, 급격하고 예측 불가한 상황에서도 견딜 수 있을 만큼 안정적인 서비스 구축

4) continuous deployments

   : 지속적인 통합/배포를 의미 

 

2. Cloud Native 아키텍처

1) 확장 가능한 아키텍처

    : 확장/수평된 형태로 시스템의 부하를 분산해 가용성을 높인다. (Scale-out)

    : 컨테이너 기반의 어플리케이션 단위 패키지를 구성하고, 모니터링을 한다.

2) 탄력적 아키텍처

    : 각 기능을 분리된 서비스로 구성하고, 자동화 파이프라인을 통해 배포해 배포 시간을 단축시킨다.

3) 장애 격리 

    : 특정 서비스에 오류가 발생해도 다른 서비스에 영향을 주지 않는다.

 

3. Cloud Native Application 특징

1) MSA로 개발된다

2) CI/CD : 지속적인 통합과 배포 

    -  지속적인 통합을 위해 jenkins, Team CI, Travis CI 를 사용할 수 있다. 

    - 지속적인 배포에는 continuous delivery(수동반영)/continuous deployment(자동반영)가 있다

    - 카나리 배포 또는 블루그린 배포 사용 가능 

3) Devops : 구현-테스트-배포를 무한 반복 

4) Containers : 컨테이너 가상화 기술 

 

아래부터는 MSA 서비스를 위한 12가지 요인들과 MSA의 특징을 정리했다.

 

4. 12 Factors 

https://12factor.net/ 

MSA 서비스를 위해 필요로 하는 12가지 요인들이다.

피보탈에서 아래에 +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는 서비스를 분리해서 개발하고 운영하는 걸 말한다.

유지보수가 쉽다. 필요한 서비스만 독립적으로 배포가 가능하다. 

https://hahahoho5915.tistory.com/71#google_vignette

 

6. MSA 표준 구성 요소

[1] MSA 표준 구성 전반 

  • 전반적인 프로세스
    • 클라이언트 => API gateway => Service Router -> Service Discovery (A서비스 찾음) => Load Banlancing통해 A서비스 중 1번 인스턴스로 접근 
  • CI/CD Automation
  • Backing Service 
  • Telemetry (Monitoring, 진단)

[2] MSA 기반 기술

- CNCF(Cloud Native Computing Foundation) https://landscape.cncf.io/

 

Cloud Native Landscape

The Cloud Native Landscape organizes all cloud native open source projects and proprietary products into categories, providing an overview of the current ecosystem

landscape.cncf.io

 

스프링부트의 Spring Cloud를 사용하면 MSA 환경을 구성할 수 있다. 

 

7. Spring Cloud

https://spring.io/projects/spring-cloud 

Spring Cloud는 개발자가 분산 시스템에서 일부 공통 패턴
(예: 구성 관리, 서비스 검색, 회로 차단기, 지능형 라우팅, 마이크로 
프록시, 제어 버스, 단기 마이크로서비스 및 계약 테스트)을 신속하게 구축할 수 있는 도구를 제공합니다.

 

스프링 클라우드의 특징들은 아래와 같으며, 다양한 내부 프로젝트들을 통해 각 특징을 구현할 수 있다.

 

  • 분산/버전 관리 구성  
  • 서비스 등록 및 검색   
  • 라우팅
  • 서비스 간 통신
  • 로드 밸런싱
  • 회로 차단기
  • 분산 메시징
  • 단기 마이크로서비스(작업)
  • 소비자 중심 및 생산자 중심 계약 테스트

수업에서는 이들중에서도, 아래의 내부 프로젝트들을 소개했다. 

- 중앙 설정 관리 : Spring Cloud Config Server 

- Location Transparency : Naming Server(Eureka) = 서비스의 위치 확인 및 검색

- Load Distribution : Ribbon 대신 Spring Cloud Gateway 사용 (최신 버전에서 권장됨)

- Easier REST clients : Feign Client

- Visibility and monitoring : Zipkin Distributed Tracing, Netflix API gateway 

- 장애 복구 : Hystrix

 

[출처]

인프런 - Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)

[참고]

https://wonit.tistory.com/490

https://hahahoho5915.tistory.com/71#google_vignette

|  스프링 부트 Test 중, 아래와 같이 오류가 나타났다.

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

}

 

[ 출처 ]

부트캠프 수업 내용 정리

|  CascadeType

     
ALL - 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 모두 전파
PERSIST - 상위 엔터티에서 저장을 하면 하위 엔터티도 저장 (영속성 전파) x.persist() 전파
MERGE - 하위 엔터티까지 병합 작업을 지속 ...(?) x.merge() 전파
REMOVE - 하위 엔터티까지 제거 작업을 지속 x.remove() 전파
REFRESH - 하위 엔터티까지 인스턴스 값 새로 고침 (다시 조회) x.refresh() 전파
DETACH - 하위 엔터티까지 엔터티 제거 x.detach() 전파

* 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();
    }
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited
public class ProductItem extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long sellerId;

    @Audited
    private String name;

    @Audited
    private Integer price;

    private Integer count;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private Product product;

    public static ProductItem of(Long sellerId, AddProductItemForm form) {
        return ProductItem.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .price(form.getPrice())
                .count(form.getCount())
                .build();
    }
}

>> 이슈! : 하위에서 삭제시, 상위로 CasecadeType.ALL이 전파되었다. (전부 다 삭제됨)

>>> 수업에서 해결할 때에는, 하위의 CascadeType.ALL을 지웠더니 해결되었다.

 

[ 출처 및 참조 ]

부트캠프 수업 내용 정리

https://data-make.tistory.com/668

 

[JPA] Spring JPA CascadeType 종류

JPA Cascade Types Spring JPA CascadeType 종류 javax.persistence.CascadeType JPA Cascade Type ALL PERSIST MERGE REMOVE REFRESH DETACH CascadeType.ALL 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 @Entity public class Person { @Id @Gen

data-make.tistory.com

https://gimmesome.tistory.com/207

 

[JPA] save와 persist차이 (save, persist, merge개념)

persist()는 리턴값이 없는 insert다. merge()는 리턴값이 없는 update다. save()는 리턴값이 있는 insert, update다. save 메소드를 호출하면.... entityInformation에서 새로운 entity이면 persist()를 그게 아니면 merge()

gimmesome.tistory.com

https://umanking.github.io/2019/04/12/jpa-persist-merge/

|  관련 용어

용어 What? How?
Lazy Loading 사용자가 보지 않는 리소스는 차후에 로딩하는 기술 - 프론트 : 무한 스크롤, placeholder 등
- 백엔드 : JPA의 지연 로딩

 

|  JPA의 Lazy Loading 

- 개발자는 JPA를 통해 프록시를 만들 수 있다.

- 프록시란, 가짜 객체를 말하는 것으로, 실제 엔터티에 대한 참조값을 가진다.

- 하나의 엔터티가 다른 엔터티와 연관관계를 맺고 있을 때 (oneToMany) , 

  연관된 객체들을 처음부터 DB에서 조회하지 않고, 실제 사용하는 시점에 DB에서 조회하면 속도를 향상시킬 수 있다.

- JPA에서는 프록시가 참조하는 객체들의 데이터 조회 시점을 정하는 타입을 Fetch Type이라 하는데, 

즉시로딩 - 한 객체를 조회할 때, 참조 객체들까지 전부 읽어온다.(EAGER),
지연로딩 - 한 객체를 조회할 때, 참조 객체들은 무시하고 해당 객체의 엔터티 데이터만 가져온다.(LAZY)

- 그 타입 중에서도 지연로딩(LAZY)이 바로 Lazy Loading이다.

 

|  General한 Lazy Loading 용어 의미

- Lazy Loading은 JPA 뿐만 아니라, 프론트에서도 사용하는 용어이다. 

- 프론트에서는 이미지나 동영상 등의 리소스들을 전부 다 올리지 않고 사용자가 필요로 할 때에만 로딩하기도 하는데,

- 이 또한 페이지의 로딩이 너무 느려지는 현상을 방지하기 위해서이다.

- 결국 정리하자면, 지연 로딩은 사용자가 당장 쓰지 않는 불필요한 데이터의 로딩을 지연시키겠다는 의미이다.

 

 

[ 참고 및 출처 ]

Lazy Loading,

https://scarlett-dev.gitbook.io/all/it/undefined-1

https://programmer-chocho.tistory.com/81

https://victorydntmd.tistory.com/210

https://velog.io/@bread_dd/JPA%EB%8A%94-%EC%99%9C-%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

|  관련 SQL 개념

(1) Unique Key : 중복 데이터 저장을 방지하기 위한 제약조건

- 단일 칼럼 뿐 아니라, 복합 칼럼을 지정할 수도 있다.

- 아래와 같이 복합 키를 생성할 경우, DB 조회 속도를 향상시킬 수 있다. 

-- 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를 써야한다.

  * 자세한 내용 : https://enterkey.tistory.com/417

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

 

[ 참고 및 출처]

* 부트캠프 수업 내용 정리

* 인덱스 개념 참조 : https://mangkyu.tistory.com/96

* Cardinality와 Selectivility : https://soft.plusblog.co.kr/87

* 인덱스 핵심 설계 문법 : https://inpa.tistory.com/entry/MYSQL-%F0%9F%93%9A-%EC%9D%B8%EB%8D%B1%EC%8A%A4index-%ED%95%B5%EC%8B%AC-%EC%84%A4%EA%B3%84-%EC%82%AC%EC%9A%A9-%EB%AC%B8%EB%B2%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

* 유니크 키를 통한 중복데이터 관리방법 https://umanking.github.io/2021/07/05/mysql-duplicate-record/

1. 운영 서버와 테스트 서버의 DB 설정을 나누고 싶을 때

(1) 먼저 application.yml 에서 개발/운영 환경에 따라 DB 설정을 나눠준다.

https://1minute-before6pm.tistory.com/12

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Config-Data-Migration-Guide#profile-groups

(2) 그리고 나서 아래와 같이 Edit Configuraton의 Active prifiles를 설정한 profile 이름으로 바꾼다.

ㄴ [추가] Build.Gradle 의존성에 대한 이해

- 너무나 복잡하게 profile을 쓰고 db를 application.yml에서 나눴는데, 그럴 필요가 없단 걸 알게 됐다.

- 그냥 아래처럼 나누면 되었었다.

- implementation : 정말 라이브러리만 추가한 것을 말한다.

- runtimeOnly : 추가할 라이브러리는 런타임 시에 반드시 필요한 라이브러리임

- compileOnly : 추가할 라이브러리는 런타임 시에 반드시 필요하진 않음

* application.yml로 datasource를 통해 tomcat 또는 spring 실행 시에 사용할 db를 연결하는 것과는 다른 맥락

 

https://toss.tech/article/how-to-manage-test-dependency-in-gradle

 

테스트 의존성 관리로 높은 품질의 테스트 코드 유지하기

혹시 테스트 코드에서도 의존성을 관리해본 적이 있으실까요? 해당 포스트에서는 Gradle의 java-test-fixtures 플러그인을 사용하여 테스트 의존성 관리를 통해 높은 품질의 테스트 코드를 유지하는

toss.tech

 

 

2. 서버 메타데이터를 클래스로 뽑아서 관리하고 싶을 때

- 인증 메일을 보낼 때, 서버의 주소가 필요한데 그걸 매번 쳐주지 않고 주입시켜주려고 했다.

- application.yml에서 메타정보를 입력해서 가져올 수도 있는데, 더 우아한 방식이 있다고 해서 써봤다.

- 방법은 아래와 같이 서버를 클래스로 따로 설정해주는 것이다.

https://www.baeldung.com/spring-boot-configuration-metadata

/* 이메일 인증 시 서버 주소 필요하여 별도로 서버 설정 */
/* 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"를 인식못한다. (이런..으귬느앎ㄴ;ㅣ러ㅏㅐㅂ쟛)

- 그래서 심플하게 개발단/운영단 나누어 작업하는 건 차후에 하기로 하고, 

  욕심부리지 않고 mysql만 사용하기로 했다. H2 진짜 어우 계속 화나게 하넴 ㅋㅋ 

 

 

|  개요

- 서비스에서 @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에 업데이트를 해준다. 

// default : dbms의 격리성 수준을 따르며, 부모와 별개로 실행되며, 읽기전용이 아님
@Transactional(isolation = Isolation.DEFAULT,
               progagation = Propagation.REQUIRED,
               readOnly = false){
    
}

(2) 삭제 시 @Transactional을 쓰지 않으면 삭제되지 않는 이유

: 이 부분은 정확하게는 모르겠지만, 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을 하더라도, 해당 메소드를 트랜잭션으로 인식해 필요한 데이터를 적시에 초기화한다. 

pf. https://zzang9ha.tistory.com/347

 

트랜잭션 개념

트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위

[출처] 코딩팩토리, https://wonit.tistory.com/462

- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.

(1) 트랜잭션의 연산 : Commit과 Rollback

- 트랜잭션은 순차적으로 Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.

- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.

(2) 트랜잭션의 상태

- 트랜잭션은 실행중(Active)이거나

  - 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)

  - 실행중 오류 발생으로 실패(Failed), 비정상적 종료로 rollback 수행(Aborted)

- 하는 총 5가지의 상태를 가질 수 있다.

(3) 트랜잭션의 특징

- 트랜잭션에는 4가지의 특징이 존재하는데,

ACID 내용
Atomic (원자성) All or Nothing : 모든 작업이 실행되거나 or 모두 실행되지 않아야 한다.

rf. 결제서비스에 관한 트랜잭션 중 갑자기 오류발생으로 끊긴다면? 
    ㄴ 결제서비스에 관한 트랜잭션 전체는 실행되지 않게 된다. 
Consistency (일관성) 트랜잭션 작업 결과는 항상 일관적이어야 한다.

* 모든 트랜잭션이 종료된 후엔 모든 DB의 제약조건을 지키고 있는 상태여야 한다.

rf. 웹툰 결제를 할 때 최소 결제 단위는 100원이어야 한다면, 
    결제 서비스 트랜잭션이 실행될 때 내 계정엔 100원 이상이 남아있어야 한다.
Isolation (격리성/독립성) 트랜잭션은 다른 트랜잭션과 독립적이어야 한다.

rf. 현실적으로 트랜잭션의 격리성을 지키기가 어렵다 (성능과 안정성의 trade-off)
- 격리성의 단계 ) 
  READ_UNCOMMITED > READ_COMMITED > REPEATABLE_READ > SERIALIZABLE
- 대체로 현장에서는 REPEATABLE_READ 를 한다고 한다.
Durability (지속성) 트랜잭션이 완료되면 영구적으로 결과에 반영되어야 한다.

* 트랜잭션은 순차적으로 commit되며, commit이 실패해도 모든 로그를 남겨 db에 반영된다.

 

|  트랜잭션을 사용해보기

1. @EnableTransactionManagement 달기

- 일일히 @Transactional 메소드에 붙이지 않고, @SpringBootApplication이 있는 클래스에 붙인다.

@SpringBootApplication
@EnableTransactionManagement
class XXXX{

}

2. 서비스 클래스 또는 각각의 메소드에 @Transactional 달기

 

|  여러 트랜잭션이 경쟁하면 생길 수 있는 문제

1. Diry Read

: 어떤 트랜잭션이 Row를 수정하는 중에 다른 트랜잭션이 해당 Row를 읽는 경우

* 계좌 정보를 읽고 잔액을 수정하는 중에 만약 다른 트랜잭션이 진입한다면?

작업1   작업2  
게시글 1번 조회 A    
게시글 1번 수정중 B 게시글 1번 조회 A
게시글 1번 반영 B 게시글 1번 수정중 C
    게시글 1번 반영 C

2. Non-repeatable Read

: 시간 간격 두고 특정 Row를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번 조회 A    
    게시글 1번 조회 A
    게시글 1번 수정중 C
게시글 1번 조회 C 게시글 1번 반영 C

3. Phantom-read

: 시간 간격을 두고 특정 범위를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번~4번 조회 A,B,C,D    
    게시글 1번 조회 A
    게시글 1번 수정중 E
게시글 1번~4번 조회 E,B,C,D 게시글 1번 반영 E

 

|  스프링에서 트랜잭션 처리를 지원하는 방식

- 여러가지 방식이 있으나, 가장 자주 사용하는 방식은 @Transactional이다.

- 이를 선언적 트랜잭션 방식이라고도 부른다.

(1) 서비스 메소드에 @Transactional 을 넣어 해당 작업이 트랜잭션임을 명시

(2) 트랜잭션 기능이 적용된 프록시 객체 생성

(3) PlatformTransaction manager가 트랜잭션 전체가 성공했는지 확인하고 Commit 또는 Rollback

- 스프링 트랜잭션의 세부 설정들

* 모든 트랜잭션을 동기화처리(synchronized) 하게 되면 속도가 너무 느려진다.

* 적절한 설정을 통해 속도와 안전성 두 마리 토끼를 잡자

pf. 전파 수준이 아직 이해가 잘 가지 않는다.. 관련 자료를 차후에 더 보고 정리하자.

https://n1tjrgns.tistory.com/266

https://www.baeldung.com/spring-transactional-propagation-isolation

세부 설정 내용
Isolation(격리수준) 트랜잭션에서 일관성이 없는 데이터를 허용하는 수준

- 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와 동일하게 작동한다.

ex. 일기작성을 부모 트랜잭션으로, 로그 저장을 자식 트랜잭션으로
- 로그 작성을 실패해도 일기 작성은 롤백되지 않는다.
- 일기 작성이 실패하면 로그 작성도 롤백된다.

rf. 예제
https://oingdaddy.tistory.com/28
ReadOnly 트랜잭션을 읽기 전용 속성으로 지정

* 성능 최적화, 특정 트랜잭션 안에서 읽기 외 작업이 일어나는 것을 방지할 때 사용

@Transaction(readOnly=true)
트랜잭션 롤백 예외 예외 발생했을 때 트랜잭션 롤백시킬 경우를 설정

@Transaction(rollbackFor=Exception.class) : 예외 상황 발생 시 롤백된다
@Transaction(noRollBackFor=Exception.class) : 어떤 예외가 되더라도 커밋된다

* 디폴트 : RuntimeException, Error
timeout 일정 시간 내에 트랜잭션 끝내지 못하면 롤백 

@Transaction(timeout=10)

 

|  정리

- 트랜잭션의 개념

  (1) 트랜잭션은, DB의 상태를 변화시키는 하나의 작업 단위, 다시 말해 CRUD 행위를 말한다.

  (2) 트랜잭션은, 커밋과 롤백이라는 두 가지 연산을 통해,
       총 5가지의 상태 (실행중, 일부 커밋, 완전 커밋, 실패, 롤백) 를 가질 수 있다.

  (3) 트랜잭션의 특징으로는 ACID가 있는데,

      - 원자성은 All or Nothing을 말하며,
      - 일관성은 작업 처리 결과는 늘 일관적이어야 한다는 의미이며, 
      - 독립성은 한 트랜잭션이 실행되고 다른 트랜잭션이 수행되어야 한다는 의미이며,
      - 지속성은 한 번 변화를 시켰으면 그것이 영구적으로 반영되어야 한다는 걸 의미한다. 

 

[ 참고 및 출처 ]

- 부트캠프 수업 내용을 정리

- https://stackoverflow.com/questions/8190926/transactional-saves-without-calling-update-method

- https://kafcamus.tistory.com/31

- https://stackoverflow.com/questions/32269192/spring-no-entitymanager-with-actual-transaction-available-for-current-thread

- https://stackoverflow.com/questions/26601032/default-fetch-type-for-one-to-one-many-to-one-and-one-to-many-in-hibernate

- 기타 출처의 경우, 페이지 내 링크 참조

1. @AutoWired 동작원리

https://beststar-1.tistory.com/40

 

@Autowired의 동작원리

@Autowired란? 의존관계 주입(DI)을 할 때 사용하는 어노테이션(Annotation)이며, 의존 객체의 타입에 해당하는 빈(Bean)을 찾아 주입하는 역할을 한다. 💡 의존관계 주입에 대해서는 IoC(Inversion of Control,

beststar-1.tistory.com

 

2. 생성자 주입과 관련한 설명

https://jackjeong.tistory.com/41

 

[Spring] 생성자 주입 vs 필드 주입 (@Autowired)

안녕하세요~ 잭코딩입니다! 이번에는 스프링 프레임워크에서 의존성을 주입하는 방법을 살펴보고 어떤 방식으로 주입하는 게 좋은지 살펴볼까요? 우선 결론부터 말하자면 생성자 주입 (Constructo

jackjeong.tistory.com

 

+ Recent posts