728x90
반응형
728x90
반응형
반응형

isolation level 이란 무엇인가?

  • 디비 동시성을 관리하고 데이터 정합성을 유지하기 위해 서로 다른 트랜젝션끼리의 관계를 정의한 것
  • 트랜잭션이 동시에 수행될때 다른 트랜잭션이 동일한 데이터에 대해서 어떻게 보일지에 대한 범위를 나타낸다.

 

존재 이유?

  • 언제 그리고 어떤 식으로 한 트랜젝션이 만든 변화를 보이게 할지(visibility) 조절하면서 정합성(consistency)과 성능(performance) 사이의 균형(balance)을 맞추기 위함

 

isolation level 4가지

Read Uncommited

  • 제일 낮은 레벨
  • 언제? 이슈 위험 높음(정확도 낮음); 성능이 중요할 때
  • 정의: 커밋되지 않은, 수정된 데이터가 다른 트랜젝션에서도 보임(dirty reads)
  • 이슈: dirty read, non repeatable read, phantom read 모두 발생

 

Read Commited

  • 기본 설정인 DB: postgreSQL, oracleDB
  • 언제? 정합성이 중요하나 반복해서 읽었을 때 같을 필요 없을 경우
  • 정의: 커밋된 수정 사항만 다른 트랜젝션에서 보임
  • 이슈: 같은 데이터를 여러번 조회할 경우 다른 결과를 볼 수도 있음; 다른 트렌젝션에서 변경했음(non repeatable reads)
    • non repeatable read, phantom read 모두 발생

 

Repeatable Read

  • 기본 설정인 DB: mySQL
  • 언제? 정합성이 중요하고 한 트랜젝션 내에서 한 데이터를 여러번 조회했을 때 같은 결과가 나와야하는 경우
  • 정의: 같은 트랜젝션 내 한 데이터를 여러번 조회할 경우 항상 같은 결과를 보장함(트랜젝션 시작 전 커밋된 데이터만 보임)
  • 이슈: 다른 트랜젝션에서 삽입/삭제된 row(혹은 변화)를 볼 수 없음(phantom reads)

 

Serializable

  • 제일 높은 레벨
  • 언제? 데이터의 무결성과 정합성이 엄청 중요할 때(금융..)
  • 정의: 마치 동기로 실행하듯 하나 끝나고 다른 하나를 실행한다.(sequentially)
  • 이슈: 성능이 안좋음; 가끔 테이블 전체를 Lock 함

 

728x90
반응형

'개발 > sql' 카테고리의 다른 글

[mysql] basic functions  (0) 2024.09.09
[mysql] collation이란  (0) 2024.06.10
[mysql] merge into..?  (0) 2024.05.17
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
반응형

환경: mysql 5.7

oracle에는 merge into 가 있어 값이 있을때는 수정하고 없을때는 추가하여 pk 없음 에러 혹은 중복키 저장 에러가 나지 않고 작업을 진행할 수 있었는데, mysql에는 같은 기능을 하는 쿼리가 있는지 알아본다.

oracle

MERGE INTO [UPDATE되거나 INSERT 될 테이블]
USING [MERGE를 진행하고 싶은 대상, 조인, 서브쿼리도 사용 가능]
ON [조건]
WHEN MATCHED THEN [조건에 맞는 데이터가 있을 시 실행할 구문, UPDATE, DELETE]
WHEN NOT MATCHED THEN [조건에 맞는 데이터가 없을 시 실행할 구문, INSERT]
;

 

mysql

INSERT INTO 테이블 (
	[콜롬들...]
)VALUES(
	[값들...]
)
ON DUPLICATE KEY UPDATE
	[PK값들..]

예시

category 테이블 pk가 service_code, category_code 인 경우, category_name을 추가하거나 수정하려고 한다면..

CREATE TABLE `category` (
  `service_code` varchar(20) NOT NULL COMMENT '서비스 코드',
  `category_code` varchar(20) NOT NULL COMMENT '카테고리 코드',
  `category_name` varchar(20) NOT NULL COMMENT '카테고리 명',
  PRIMARY KEY (`service_code`,`category_code`)

 

INSERT INTO category 
	(service_code, category_code, category_name) 
VALUES
	('admin', 'character', '캐릭터')
ON DUPLICATE KEY UPDATE 
	service_code = 'admin' , category_code = 'character'
;

위처럼 쓸 수도 있고 아래처럼 작성해도 동일하다.

INSERT INTO category 
	(service_code, category_code, category_name) 
VALUES
	('admin', 'character', '캐릭터')
ON DUPLICATE KEY UPDATE 
	service_code = VALUES(service_code), category_code = VALUES(category_code)
;

날려보면 아래처럼 성공하는 로그가 찍힌다.

하지만 실제 row는 수정되지 않았다! 그렇다고 신규 row가 생기지도 않았다.

참고로 기존 row를 지우고 날리면 1로 결과가 떨어지고 신규 row가 추가된다. 허나 이 상태에서 category name을 바꾸고 날려보면 1로 떨어져도 반응이 없다..

 

쿼리를 아래처럼 수정하면 

INSERT INTO category (service_code, category_code, category_name) 
VALUES ('admin', 'character', '캐릭터797')
ON DUPLICATE KEY UPDATE 
    service_code = VALUES(service_code),
    category_code = VALUES(category_code),
    category_name = VALUES(category_name);

기존 row가 있을 경우 결과가 2로 떨어지고 데이터도 수정된 것을 확인할 수 있다. 없을 경우 1로 떨어지고 추가된다.

왜?!???

 

With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values. If you specify the CLIENT_FOUND_ROWS flag to the mysql_real_connect() C API function when connecting to mysqld, the affected-rows value is 1 (not 0) if an existing row is set to its current values.

결과에 대해 더 찾아보니 아래와 같이 내린다는 것을 알게 되었다.

  • 1: 신규 row로 insert
  • 2: 기존 row update
  • 0: 변경 없음

 참고

mysql 5.7의 경우

https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html

 

MySQL :: MySQL 5.7 Reference Manual :: 13.2.5.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

13.2.5.2 INSERT ... ON DUPLICATE KEY UPDATE Statement If you specify an ON DUPLICATE KEY UPDATE clause and a row to be inserted would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row occurs. For example, if column a is de

dev.mysql.com

 

mysql 8부터는 아래 문서를 확인해야 한다.

https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

 

MySQL :: MySQL 8.0 Reference Manual :: 15.2.7.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

15.2.7.2 INSERT ... ON DUPLICATE KEY UPDATE Statement If you specify an ON DUPLICATE KEY UPDATE clause and a row to be inserted would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row occurs. For example, if column a is de

dev.mysql.com

 

 

728x90
반응형

'개발 > sql' 카테고리의 다른 글

[mysql] collation이란  (0) 2024.06.10
DB isolation level  (0) 2024.05.22
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
[형상관리] flyway vs liquibase  (0) 2022.07.08
반응형

try with resources

  • from java7; enhanced in java9
  • try 블락 안에 열린 리소스를 예외 여부와 상관없이 자동으로 닫아주는 기법
    • stream, db, network.. 등

 

예시

try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    // Exception handling
}

: BufferedReader, FileReader 와 같은 리소스의 close함수를 호출하지 않아도 자동으로 닫아줌

 

어떤 클래스들이 자동으로 닫히나?

AutoClosable interface 혹은 이를 확장한 AutoCloaseable interface를 구현한 리소스

public interface AutoCloseable {
    void close() throws Exception;
}
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

이를 구현한 예시로는

  • 스트림: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter
  • reader/writer: InputStreamReader, OutputStreamWriter
  • sql connection: java.sql.Connection, java.sql.ResultSet, java.sql.Statement
  • 소켓: java.net.Socket
  • 채널: java.nio.channels > FileChannel, SocketChannel, ServerSocketChannel
  • zip: java.util.zip.ZipFile

 

728x90
반응형
반응형

객체를 ordering 하는 방법에 대해 알아본다.

 

Comparable interface

  • 객체의 natural ordering 에 대해 정의할 수 있음
  • 클래스 안에서 클래스끼리 비교할 때 override해두면 Collections.sort, Arrays.sort 등에서 사용됨
  • 하나뿐인 아래 함수를 상속받고 구현하면 됨, 구현해야지만 사용할 수 있음(not functional interface)
public int compareTo(T o);
  • 해당 함수로 현재 객체(this)와 다른 객체(o)를 비교할 수 있으며 반환 값은 아래와 같음
    • this < o : -1    정방향
    • this == o : 0
    • this > o : 1     역방향
  • 참고로 String 객체는 해당 interface의 함수를 이미 구현하고 있어서 별도의 설정을 하지 않아도 알파벳 순 정렬을 할 수 있다.

String.java

import java.util.*;

@Getter
@ToString
@AllArgsConstructor
public class Person implements Comparable<Person> { ///
    private String name;
    private int age;

    @Override
    public int compareTo(Person other) { ///
        return Integer.compare(this.age, other.age); // Natural order by age
    }



    // Main method for testing
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 35)
        );

        // Sort using the natural order defined by Comparable
        // 안줘도 자동으로 있는거 사용
        Collections.sort(people);

        System.out.println("Sorted by age (natural order): " + people);
    }
}

 

Comparator Interface

  • Comparator is intended to be used for defining external comparison logic rather than internal natural ordering. However, you can create a Comparator for Person as a separate class, an inner class, or even as a static field in the Person class to provide custom sorting criteria.
  • 객체 자체의 natural ordering 이 없거나, 좀 더 복잡한 정렬 방법이 있을 경우 사용(flexibility)
  • 아래 함수를 상속받고 구현하면 됨(functional interface) ; 람다로 사용가능
int compare(T o1, T o2);
  • int를 반환하도록 되어있는데, 비교 값은 아래와 같다.
    • 앞 < 뒤 : -1    정방향
    • 앞 == 뒤 : 0
    • 앞 > 뒤 : 1     역방햐
  • String 객체에 역시 이미 정의 되어 있다.

String.java

기본함수

 

  • Comparator<T> reversed():
    • Returns a comparator that reverses the order of this comparator.
  • Comparator<T> thenComparing(Comparator<? super T> other):
    • Returns a comparator that first compares using this comparator, and if the comparison is equal, uses the provided comparator. 앞에 비교한 결과가 같으면 추가 사용!

 

import java.util.*;

@Getter
@ToString
@AllArgsConstructor
public class Person { //여기서 comparator implement못하고 별도의 클래스로 빼야 함
    private String name;
    private int age;


    // Main method for testing
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 35)
        );

        // Sort by name
        // 별도로 만들어서 사용 , 람다 사용가능
        Comparator<Person> nameComparator = Comparator.comparing(Person::getName);
        Collections.sort(people, nameComparator);
        System.out.println("Sorted by name: " + people);

        // Sort by age in reverse order
        Comparator<Person> ageComparator = Comparator.comparingInt(Person::getAge).reversed();
        Collections.sort(people, ageComparator);
        System.out.println("Sorted by age (reverse order): " + people);
    }
}
//or 아래처럼 함수로 빼고 new AgeComparator() 삽입
   // Comparator for sorting by age
    public static class AgeComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return Integer.compare(p1.getAge(), p2.getAge());
        }
    }

 

어떤 것을 사용해야 하나

The Comparable interface is a good choice to use for defining the default ordering, or in other words, if it’s the main way of comparing objects.

기본적으로는 Comparable 을 사용하고 아래와 같은 경우 Comparator을 사용

  • 파일이 수정 불가하여 interface를 구현할 수 없는 경우 Comparator 사용
  • 비교 로직을 분리해서 관리해야하는 경우 Comparator 사용
  • 비교 전략이 다양해서 Comparable 로 부족할 경우 Comparator 사용

 


참고

https://www.baeldung.com/java-comparator-comparable

728x90
반응형
반응형
  1. Synchronization: Use the synchronized keyword to ensure that only one thread can access a critical section of code at a time. This helps prevent race conditions where multiple threads might try to modify shared data simultaneously.
  2. Locks: Java provides various lock implementations like ReentrantLock, ReadWriteLock, and StampedLock which offer more flexibility and functionality compared to intrinsic locks (synchronized). They llow finer control over locking mechanisms and can help avoid deadlock situations.
  3. Thread-safe data structures: Instead of using standard collections like ArrayList or HashMap, consider using their thread-safe counterparts from the java.util.concurrent package, such as ConcurrentHashMap, CopyOnWriteArrayList, and ConcurrentLinkedQueue. These data structures are designed to be safely accessed by multiple threads without external synchronization.
  4. Atomic variables: Java's java.util.concurrent.atomic package provides atomic variables like AtomicInteger, AtomicLong, etc., which ensure that read-modify-write operations are performed atomically without the need for explicit locking.
  5. Immutable objects: Immutable objects are inherently thread-safe because their state cannot be modified once created. If an object's state needs to be shared among multiple threads, consider making it immutable.

멀티 스레드 환경에서 안전하게 코딩하는 기본적인 다섯가지 방법에 대해 살펴본다. 자바 기준

1. synchronized 키워드 사용(직접적인 락은 아니지만 락과 같은 것;  monitor lock)

  • 함수 자체 그리고 함수 안에서도 블락으로 지정하여 사용 가능
  • allowing only one thread to execute at any given time.
  • The lock behind the synchronized methods and blocks is a reentrant. This means the current thread can acquire the same synchronized lock over and over again while holding it(https://www.baeldung.com/java-synchronized

2. 명시적으로 lock interface를 구현한 구현체를 사용 

lock()/tryLock() 함수로 락을 걸고 unlock()함수로 반드시 락을 해제해야한다.(아니면 데드락..)

Condition 클래스를 통해 락에 대한 상세한 조절이 가능

  • ReentrantLock implements Lock
    • synchronized와 같은 방법으로 동시성과 메모리를 핸들링하지만 더 섬세한 사용이 가능하다.
  • ReentrantReadWriteLock implements ReadWriteLock
    • Read Lock – 쓰기락이 없거나 쓰기락을 요청하는 스레드가 없다면, 멀티 스레드가 락 소유 가능
    • Write Lock – 쓰기/읽기 락 모두가 없는 경우 반드시 하나의 스레드만 락을 소유한다.
  • StampedLock(자바8 도입, 읽기/쓰기 락 모두 제공)
    • 락을 걸면 long 타입의 stamp를 반환함 해당 값으로 락을 해제하거나 확인 가능
    • optimistic locking임(수정사항이 별로 없다는 가정하에 읽기에 개방적)
    • https://www.baeldung.com/java-concurrent-locks

3. thread safe 한 자료구조 사용

  • java.util.concurrent 패키지 참고(from java 5)
    • ConcurrentHashMap, ConcurentLinkedQueue, ConcurrentLinkedDeque 등
  1. ConcurrentHashMap: This class is a thread-safe implementation of the Map interface. It allows multiple threads to read and modify the map concurrently without blocking each other. It achieves this by dividing the map into segments, each of which is independently locked.
  2. CopyOnWriteArrayList: This class is a thread-safe variant of ArrayList. It creates a new copy of the underlying array every time it is modified, ensuring that iterators won't throw ConcurrentModificationException. This makes it suitable for scenarios where reads are far more frequent than writes.
  3. ConcurrentLinkedQueue: This class is a thread-safe implementation of the Queue interface. It is designed for use in concurrent environments where multiple threads may concurrently add or remove elements from the queue. It uses non-blocking algorithms to ensure thread safety.
  4. BlockingQueue: This is an interface that represents a thread-safe queue with blocking operations. Implementations like LinkedBlockingQueue and ArrayBlockingQueue provide blocking methods like put() and take() which wait until the queue is non-empty or non-full before proceeding.
  5. ConcurrentSkipListMap and ConcurrentSkipListSet: These classes provide thread-safe implementations of sorted maps and sets, respectively. They are based on skip-list data structures and support concurrent access and updates.

4. thread safe 한 변수 사용

  • java.util.concurrent.atomic 패키지 참고(from java 5)
    • AtomicInteger, AtomocLong, AtomicBoolean 등
    • 스레드 끼리 동기화 필요한 변수에 사용(상태 공유 등); 관련 성능 향상 시
    • 락으로 인한 오버헤드나 컨테스트 스위칭 비용 감소
    • non blocking 상황에서(스레드 별 독립적인 작업 시)

5. immutable object 불변 객체 사용

  • String, Integer, Long, Double 등 과 같은 wrapper 클래스 사용
  • private final로 선언
  • 생성자 이용하여 initialize
  • setter 함수나 수정가능 함수 제공하지 않기

6. volatile 

  • 변수를 Main Memory에 저장하겠다고 명시하는 것
  • 각 스레드는 메인 메모리로 부터 값을 복사해 CPU 캐시에 저장하여 작업하는데 volatile은 CPU 캐시 사용 막고 메모리에 접근해서 실제 값을 읽어오도록 설정하여 데이터 불일치를 막음
  • 자원의 가시성: 메인 메모리에 저장된 실제 자원의 값을 볼 수 있는 것
  • 멀티쓰레드 환경에서 하나의 쓰레드만 read&write하고 나머지 쓰레드가 read하는 상황에 사용
    • Read : CPU cache에 저장된 값 X , 메인 메모리에서 읽음
    • Write : 메인 메모리에 작성
    • CPU Cache보다 메인 메모리가 비용이 더 큼(성능 주의)
  • 가장 최신 값 보장

what to choose

synchronized

  • 여러 쓰레드가 write하는 상황에 적합
  • 가시성 문제해결 : synchronized블락 진입 전/후에 메인 메모리와 CPU 캐시 메모리의 값을 동기화 하여 문제 없도록 처리

volatile

  • 하나의 쓰레드만 read&write하고 나머지 쓰레드가 read하는 상황에 적합
  • 가시성 문제해결 : CPU 캐시 사용 막음 → 메모리에 접근해서 실제 값을 읽어오게 함
    • Read : CPU cache에 저장된 값 X , 메인 메모리에서 읽음
    • Write : 메인 메모리에 작성

AtomicIntger

  • 여러 쓰레드가 read&write를 병행
  • 가시성 문제해결: CAS알고리즘
    • 현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교하여
      • 일치할 경우 새로운 값으로 교체
      • 불일치할 경우 실패하고 재시도
728x90
반응형
반응형

환경: springboot 2.6.2, spring-web

 

아래와 같은 컨트롤러가 있다.

@RequestMapping("/api/member")
@RestController
@RequiredArgsConstructor
public class MemberController {

  private final UserService userService;

 
  @GetMapping("/loss-game-money")
  public BaseResponse<MemberLossGameMoneyResponse> getMemberLossGameMoney(@ModelAttribute MemberLossGameMoneyRequest request) {
    return userService.getMemberLossGameMoney(request);
  }

  
  @GetMapping("/{id}")
  public BaseResponse<MemberInfoResponse> getMember(@PathVariable String id) {
    return new BaseResponse<>(userService.getMemberInfo(id));
  }
}

 

이 상황에서 아래 api를 요청한다면? endpoint가 정의되어 있지 않아 404가 날 것이라 기대했다.

http://localhost:8600/api/member

하지만 200 ok 가 떨어졌다.

 

분명 컨트롤러에 정의되어 있지 않고, 그렇다고 에러 내용이 controller advice에 정의되어 있지도 않은데.. 

그리고 과거에는 404로 떨어졌던 기억도 있던 터라 구글링을 해본다..

 

의심 1. 스프링 버전 이슈?

구글링 하다가 스프링 버전 2.3 이후부터 바뀐 스펙이라고 적힌 것을 봐서.. 스프링 버전의 문제인가 의심했다.(개소리로 판명)

The change in behavior where Spring Boot started returning a 200 OK response with an empty body for unmatched endpoints instead of a 404 response happened around Spring Boot version 2.3.x.
In versions prior to 2.3.x, the default behavior was to return a 404 Not Found response for unmatched endpoints. However, starting from version 2.3.x, the default behavior was changed to return a 200 OK response with an empty body.

공식 문서를 찾다가 실패하여 신규 프로젝트에 버전을 아래와 같이 중간 버전만 하나씩 올려서 테스트해 봤는데

  • 테스트해 본 버전: 2.2.6.RELEASE, 2.3.4.RELEASE, 2.4.5, 2.5.6, 2.6.3, 2.7.3, 2.7.10, 2.7.18, 3.0.0, 3.2.4(현재 최신)

절대 재현되지 않는다.. 버전 문제는 아니고 소스 문제라고 판명..

아래와 같이 모두 동일한 결과를 return 한다. 404 잘만 나는구먼..

없는 주소로 요청할 경우 404

 

의심 2. controllerAdvice...?

혹시 exception handler가 잡아서 200으로 반환하나 싶어 소스를 뒤져봐도 그런 건 없었다.

 

의심 3. 그럼 필터? 인터셉터?

혹시나 싶어 필터나 인터셉터를 하나씩 주석해 보고 실행해 본다.

로그를 살펴보니 필터에 해당하는 로그는 잘 찍히고 있어 필터 이후에서 200을 반환하는 것이라 판단했다.

그리고 인터셉터를 하나씩 보는데.. 잡았다 요놈..

@Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if (handler instanceof HandlerMethod) {
    	...
      return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    return false; ///
  }

위 함수에서 컨트롤러에 정의된 api는 if (handler instanceof HandlerMethod) 구문에 true를 반환하여 작업을 하고 마지막에 true를 반환하지만 정의되지 않은 api는 false를 타게 된다. false를 타면 상태코드 200에 empty body로 리턴된다..! false를 반환한다는 의미가 controller를 타지 않고 작업을 종료한다는 의미고 그게 잘 되었으니 어쩌면 맞을 수도..

혹시나 싶어서 신규 프로젝트로 기본 세팅만 한 후 재현해 본다.

1. @EnableWebMvc 없고 + return false

@Component
public class TestInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return false;
  }
}
@Component
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

  private final TestInterceptor interceptor;
  
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
  //TestInterceptor가 이미 빈으로 등록되어 있으므로 주입하여 사용하기위해 아래 주석;
  //TestInterceptor에 @Component가 없으면 빈이 아니므로 아래 주석이 작동함
//    registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
    registry.addInterceptor(interceptor);
  }
}
@RequestMapping("/api")
@RestController
public class TestController {

  @GetMapping("/test")
  public String test( ) {
    return "hello";
  }
}
  • interceptor 설정 전
    • http://localhost:8080/api/test 요청 시
      • 200 hello
    • http://localhost:8080/api/test3 요청 시
      • 404 기본 404 not found 메시지
  • interceptor 설정 후
    • http://localhost:8080/api/test 요청 시
      • 200 empty body
    • http://localhost:8080/api/test3 요청 시
      • 200 empty body

 

200이 아닌 다른 상태 값으로 반환하려면 아래와 같이 수정하면 된다.

empty body에 404로 떨어진다.

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  if (! (handler instanceof HandlerMethod)) {
    response.setStatus(404); //
    return false;
  }
  return true;
}

 

이를 수정해 본다면? 에러를 던져보자.

2. @EnableWebMvc 없고 + throw exception

아래와 같이 false 반환대신 에러를 던지면 throw 절을 만나게 되고 BasicErrorController를 타고 500으로 떨어진다.

(어떤 종류의 exception이건 500으로 떨어진다. 그 이유는 Exception이 처리가 이뤄지지 않은 상태로 WAS에게 전달되었기 때문이다. WAS 입장에서는 처리되지 않은 Exception을 받으면 이를 예상치 못한 문제로 인해 발생했다고 간주하고 status코드 500에 Internal Server Error를 메시지에 설정하게 된다.)

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  if (! (handler instanceof HandlerMethod)) {
    throw new RuntimeException("nono");
  }
  return true;
}

 

@EnableWebMvc 어노테이션의 유무

1. @EnableWebMvc 있고 + 인터셉터에서 return true로 진행시키면

여기서 @EnableWebMvc 어노테이션을 추가하면 (어떤 에러를 던지건) 404로 에러가 바뀐다....

spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..(아래에서 계속)

더 확인해 보니 @EnableWebMvc를 선언하면 아래 부분이 true로 내려와서

handler instanceof HandlerMethod == true

결국 preHandler가 true로 return 되어 에러를 만나지 않고, controller 단에 가서 url을 찾다가 없어서 404 -> BasicErrorController를 타는 플로우였다.

2. @EnableWebMvc + 인터셉터 내부에서 에러 발생 시키면

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  throw new HttpClientErrorException(HttpStatus.MULTI_STATUS);
}
2024-04-18 09:56:29.978  WARN 12950 --- [nio-8080-exec-8] o.s.web.servlet.PageNotFound             : No mapping for GET /api/test3
2024-04-18 09:56:29.980 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
	at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:16) ~[main/:na]
	...

2024-04-18 09:56:29.982 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[Tomcat].[localhost]           : Exception Processing ErrorPage[errorCode=0, location=/error]

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22]
    ...

 

해당 부분에서도 항상 에러를 뱉게 하면 에러가 발생해도 404로 떨어진다.

그 의미는 에러가 발생해도 controller 쪽으로 요청이 들어온다는 것인데, 그 이유는 WAS의 에러페이지를 위한 요청 때문이다.

https://cs-ssupport.tistory.com/494

 

interceptor에 에러를 반환하고 exception handler를 통해 에러를 반환하려고 하면 어떻게 해야 할까?

interceptor는 dispatcher servlet 이후 스프링 컨텍스트 안에 있기 때문에 controller advice를 통한 처리가 가능하다.

1. @EnableWebMvc + controllerAdvice

@EnableWebMvc를 선언한 상태에서 interceptor에서 에러를 뱉게 아래와 같이 수정하고

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  throw new RuntimeException("yesyes");
}

controllerAdvice를 통해 exception handler를 작성하면 의도한 대로 상태코드 400에 에러 메시지가 나온다!

@RestControllerAdvice
public class InterceptorExceptionHandler {

  @ExceptionHandler({RuntimeException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public String error(RuntimeException exception){
    return "Exception : " + exception.getMessage();
  }
}

WARN 23846 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound             : No mapping for GET /api/test3

로그에는 page not found 가 적힌다.

 

2. @EnableWebMvc 제거 + controllerAdvice

여기서 @EnableWebMvc를 선언을 제거한다면?

그래도 api는 동일한 결과가 반환된다.

그런데 로그는 다르게 찍힌다. 반환한 에러에 대한 값이 찍힌다.

ERROR 24122 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: yesyes] with root cause

java.lang.RuntimeException: yesyes
	at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:14) ~[main/:na]
	at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:148) ~[spring-webmvc-5.3.22.jar:5.3.22]
	...

 

@EnableWebMvc 어노테이션 관련..

spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..

에 대해 좀 더 알아본다.

우선 맞다. springboot에 spring boot starter web 디펜덴시가 있으면 @SpringBootApplicaion 어노테이션에 의해 @EnableAutoConfiguration 어노테이션이 작동하고 웹의 기능(DispatcherServlet 등)을 사용하기 위한 기본 세팅을 자동으로 해준다.

@EnableWebMvc 어노테이션이 사용되면 스프링은 웹 애플리케이션을 위한 준비를 하게 되는데, 기본 설정값으로 아래 클래스를 읽어온다. 

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
The @EnableWebMvc annotation is used in Spring to enable the Spring MVC framework for a web application. When this annotation is used, it imports the Spring MVC configuration from WebMvcConfigurationSupport
 and helps in setting up the necessary components for handling web requests, such as controllers, views, and more. This annotation is often used in conjunction with the @Configuration annotation to enable Spring MVC configuration in a Java-based configuration approach.

 

우리가 기본 세팅을 커스터마이징 하려면 WebMvcConfigurerAdapter라는 클래스를 extend 해야 하는데, 이 클래스는 spring5.0 이래로 deprecated 되었고, WebMvcConfigurer interface를 implement 해야 한다.

그런데 여기서 주의해야 할 점은 커스텀 세팅을 사용하려면 @EnableWebMvc 어노테이션이 없어야 한다는 것이다.

만약 @EnableWebMvc 어노테이션과 커스텀 세팅을 사용하려면 빈으로 등록된 DelegatingWebMvcConfiguration 클래스를 확장하여 재정의해야 한다. 기본 값을 사용하지 않기에 모든 함수를 재정의해야 함에 주의하자.

https://docs.spring.io/spring-boot/docs/current/reference/html/web.html

https://dev.to/xterm/be-careful-when-using-configuration-classes-with-enablewebmvc-in-spring-boot-2n32

 

Be careful when using @Configuration classes with @EnableWebMvc in Spring Boot

Situation Recently, we have been faced with a strange issue after adding a configuration...

dev.to

위에서 @EnableWebMvc 어노테이션 유무에 따라 결과가 다르게 나온 이유는 아마도,

@EnableWebMvc 어노테이션 + 커스텀 세팅(인터셉터)인데 DelegatingWebMvcConfiguration 클래스의 재정의가 없어 기본 세팅값으로 override 되어 다른 결과가 나온 것이 아닌가 추측된다.

 


위 내용을 학습하다 스프링 버전 3부터 루트 매핑(@RequestMapping("")과 @RequestMapping("/"))에 대한 이슈가 있다는 글을 보아서 참고로 링크 남긴다.. 관련 글 때문에 더 헷갈렸다.

https://github.com/spring-projects/spring-boot/issues/33499

 

404 error occurs with RequestMapping(path="") · Issue #33499 · spring-projects/spring-boot

Used version SpringBoot3.0.0 GA Occurrence event When I specify an empty string in Controller's RequestMapping (@RequestMappint(path="")) and access the root path, The mapping doesn't work properly...

github.com


interceptor in spring context: was 흐름까지 정리 https://cs-ssupport.tistory.com/494

 

[Spring] 스프링 인터셉터

[Spring] Servlet "Filter" 현재 Controller에 의해서 매핑되는 Page가 다음 종류가 있다고 하자 1. 로그인 하지 않고 접근 가능 2. 로그인 해야 접근 가능 >> 여기서 과연 "로그인 해야 접근 가능한 페이지"가

cs-ssupport.tistory.com

https://velog.io/@monkeydugi/Spring-Interceptor%EC%97%90%EC%84%9C-%EC%98%88%EC%99%B8%EB%A5%BC-%EC%9D%91%EB%8B%B5-%ED%95%B4%EC%A3%BC%EB%8A%94-%EB%B0%A9%EB%B2%95

 

Spring Interceptor에서 예외를 응답 해주는 방법

상황이 어떤가 살펴보자.소셜 로그인을 시도한다.authorization code가 유효하지 않으면, 예외를 발생 시킨다.500으로 응답한다.위의 코드는 인터셉터에서 예외를 발생 시킨다.하지만 이렇게 끝내면 5

velog.io

 

728x90
반응형
반응형

기업에서 서버팜을 구축하게 되면 어플리케이션 서버 전에 여러 단계의 라우터나 스위치, 로드발랜서 등등이 구축되어 실제 요청한 사람의 ip를 알기 어려워지게 된다.

소스로 말하자면 아래 값이 로컬이 나오거나 내부 장비의 ip가 찍히는 상황이 생기게 된다.

HttpServletRequest request;
request.getRemoteAddr();

 

그리하여 실제로 요청한 사람/장비의 ip를 알기 어려워지는데.. 아래와 같이 설정하면 해결할 수 있다.

 

1. nginx 설정

1-1. nginx map 설정(생략가능)

map은 다음과 같이 $key라는 변수를 받아 $value라는 결과값을 매핑해준다.
아래 코드에서 $key 값이 a라면, $value 값은 1이다.

map $key $value {
  a 1;
  b 2;
  default 0;
}

map 규칙은 위처럼 쓸 수도 있지만, 다른 파일에 분리해 둔 뒤 받아올 수도 있다.
가령 위의 규칙을 map-rule이라는 파일에 아래와 같이 분리하면:

a 1;
b 2;

다음과 같이 파일의 상대 include할 수 있다:

map $key $value {
  include PATH/TO/map-rule; # map-rule 파일의 절대 경로
  default 0;
}

 

위 map을 사용하여 아래처럼 clientip 라는 변수에 값을 담는다.

    map $http_x_forwarded_for $clientip {
        "" $remote_addr;
        default $http_x_forwarded_for;
    }

 

1-2. server.location 세팅

clientip를 아래와 같이 세팅한다. 

  proxy_set_header   X-Forwarded-For  $clientip;

대략적인 큰 그림은 아래와 같다.

 server {
        listen       80;
        server_name  server.abc.com;
        server_tokens off;
        
        ...
        
        location / {
        	//여기부터
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $clientip;
            여기까지//
            proxy_set_header Connection "";
            proxy_http_version 1.1;
            proxy_pass http://localhost:8200;
        }
    }

 

1-3. nginx reload

nginx -s reload

 

2. 소스 변경

애플리케이션에서 X-Forwarded-For 헤더를 사용하도록 아래 두 가지 방법 중 하나를 적용해야 한다.
  1. HttpServletRequestWrapper 를 사용해서 getRemoteAddr 를 상속 -> 별도로 필터 개발 필요
  2. server.tomcat.remoteip.remote-ip-header 프로퍼티 설정

2번 방식이 더 쉽고 빠르다고 생각하여 적용해본다.

적용하기 전에, 버전 별로 키값이 달라지니 스프링 공식 문서는 꼭 확인해보자.

https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.webserver.use-behind-a-proxy-server

 

“How-to” Guides

Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework’s spring-jcl module. To use Logback, you need to include it and spring-jcl on the classpath. The recommended way to do th

docs.spring.io

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.server

 

Common Application Properties

 

docs.spring.io

지금 상황에서 연관있는 값은 아래 값인 것 같다. 기본 값은 없다.

# springboot2.2 버전 이상이면
server.tomcat.remoteip.remote-ip-header=X-FORWARDED-FOR

# 구버전
#server.tomcat.remote-ip-header=X-FORWARDED-FOR

경우에 따라서는 아래 값도 조정해야할 수 있다.

server.tomcat.remoteip.internal-proxies

이 값은 신뢰할 내부 프록시를 매칭하는 값이다. 정규식으로 작성해야하며 기본값은 아래와 같다. 일반적으로는 기본값으로도 사용가능하지만 프로덕션 환경에서 내부 프록시 ip들이 해당 범위를 넘어갈 수도 있으니 확인이 필요하다. 참고로 공백으로 설정하면 모든 프록시를 신뢰한다는 뜻으로 프로덕션 환경에서는 이렇게 설정하면 위험할 수 있으니 주의해야 한다.

  • 10/8
  • 192.168/16
  • 169.254/16
  • 127/8
  • 등등(공식 문서 확인 필요)

 

728x90
반응형

'서버 세팅 & tool > nginx' 카테고리의 다른 글

라이브환경 인증서 교체  (0) 2024.01.08
[이슈해결][apache] 304 NOT_MODIFIED  (0) 2023.10.12
[nginx] WAF  (0) 2022.03.30
[nginx] API gateway  (0) 2022.03.14
[nginx] 실전 cors 해결하기  (0) 2022.03.14
반응형

event driven architecture

정의:

  • EDA는 시스템 내의 이벤트를 통해 컴포넌트나 서비스 간의 통신을 관리하는 아키텍처입니다. 어떤 상태 변화나 동작이 발생하면 이벤트가 생성되고, 이를 기반으로 다른 컴포넌트들이 반응합니다.
  • EDA 이벤트를 통해 시스템 간 통신과 상호작용을 관리하는 데 초점을 맞추며, 컴포넌트 간의 느슨한 결합과 비동기 처리를 가능하게 합니다.

특징:

  • 비동기 통신: 이벤트는 비동기적으로 발행되고, 이를 처리하는 소비자들이 독립적으로 이벤트를 처리합니다. 이는 시스템의 느슨한 결합을 가능하게 합니다.
  • 루스 커플링: 이벤트 발행자와 소비자는 서로 직접적으로 알 필요가 없습니다. 이는 시스템 간의 의존성을 줄이고 유연한 확장성을 제공합니다.
  • 확장성: 새로운 이벤트 소비자를 쉽게 추가할 수 있고, 시스템의 각 구성 요소가 독립적으로 확장 가능합니다.
  • 리액티브 시스템: EDA는 시스템의 비동기적 반응성을 높여 실시간으로 변화에 대응할 수 있게 합니다.
  • EDA: 시스템 간의 비동기적 상호작용을 통해 최종 일관성(Eventual Consistency)을 유지할 수 있습니다. 메시지 손실이나 중복 처리 등은 메시징 시스템의 특성에 따라 다르게 처리될 수 있습니다.
  •  monolithic
    • 단일 데이터베이스
    • 트랜잭션 처리를 완벽하게 -> ACID
      • atomicity
      • consistency
      • isloation
      • durable
  • msa
    • 각 서비스마다 독립적인 데이터베이스(polyglot)
    • API를 통해 접근
    • atomicy, consistency를 완벽하게 지키기 힘듦 -> commit transaction 사용
      • 작업 단위가 정상적으로 끝났음을 알려줌
      • A 서비스: 최초에 대기상태로 저장 후 요청(카프카) -> B서비스: 수량 등을 확인하고 재고처리 후 confirm 메세지 발행(카프카; 메세지 종류는 상태에 따라 다르게 발생) -> A서비스: confirm message를 받아서 상태값을 변경
      • 롤백이나 취소 상태로 변경 용이 

event sourcing

정의:

  • Event Sourcing은 시스템의 상태를 데이터베이스에 직접 저장하지 않고, 상태 변화를 나타내는 모든 이벤트를 저장하고, 이 이벤트들을 순차적으로 재생(Replay)하여 현재 상태를 복원하는 아키텍처 패턴입니다.
  • Event Sourcing은 시스템의 상태 변화를 이벤트로 기록하여 현재 상태를 유지하고, 데이터의 변경 이력을 완벽하게 보존하는 데 중점을 둡니다.

특징:

  • 이벤트 저장: 애플리케이션의 모든 상태 변화는 이벤트로 표현되어 이벤트 스토어에 기록됩니다. 예를 들어, "계좌 개설", "잔고 추가", "잔고 인출"과 같은 이벤트들이 저장됩니다.
  • 현재 상태 재생: 현재 상태는 저장된 모든 이벤트를 순차적으로 재생하여 계산할 수 있습니다. 이를 통해 애플리케이션의 상태를 특정 시점으로 되돌리거나 복원할 수 있습니다.
  • 이벤트 불변성: 이벤트는 불변이며, 한번 기록된 이벤트는 변경되지 않습니다. 이는 감사(audit) 기록이나 상태 추적에 유리합니다.
  • CQS와의 결합: Event Sourcing은 일반적으로 CQRS 패턴과 함께 사용되며, 명령(Command)이 발생하면 이벤트로 기록하고, 이 이벤트를 통해 읽기 모델(Query)를 업데이트합니다.
  • Event Sourcing: 이벤트 로그가 시스템의 단일 진실 원천(Single Source of Truth)입니다. 모든 상태는 이벤트 로그를 통해 재구성될 수 있기 때문에 데이터 일관성이 매우 중요합니다.
  • 데이터의 마지막 상태만을 저장하는 것이 아닌, 해당 데이터에 수행된 전체 이력을 기록
    • 이벤트 자체를 발행, 끌어가서 알아서 처리
    • insert만 존재; update/delete 없음
  • 데이터 구조 단순
  • 데이터의 일관성과 트랜잭션 처리 가능
  • 데이터 저장소의 개체를 직접 업데이트 하지 않기 때문에 동시성 충돌 문제 해결 
  • 도메인 주도 설계(domain driven design)
    • aggregate; 데이터의 상태 값을 바꾸기 위한 방법 
    • projection; 현재 데이터 상태 값을 표시하기 위한 방법
  • 메세지 중심의 비동기 작업 처리
  • 단점
    • 모든 이벤트에 대한 복원; 시간
      • snapshot(중간 세이브포인트; 어느 순간 이후부터 히스토리 쌓기)
    • 다양한 데이터가 여러 조회
      • cqrs(command and query responsibility segregation)도입으로 해결 가능
        • 명령과 조회의 책임 분리
        • 상태를 변경하는 command
        • 조회를 담당하는 query


saga pattern

SAGA 패턴은 주로 비동기적으로 분산 트랜잭션을 관리하기 위해 사용됩니다. 이 패턴은 분산 시스템에서 트랜잭션 일관성을 유지하는 데 유용하며, 특히 데이터 일관성이 필요한 여러 서비스 간의 장기 실행 트랜잭션(Long-Running Transactions)에 적합합니다.

Saga는 여러 서비스에 걸쳐 진행되는 일련의 로컬 트랜잭션으로 이루어져 있습니다. 각 서비스가 자신의 트랜잭션을 성공적으로 처리하면, 다음 서비스가 트랜잭션을 진행하고, 만약 어느 한 서비스에서 트랜잭션이 실패하면 이전에 성공한 트랜잭션을 보상 트랜잭션을 통해 취소합니다.

  • 구현방식: 어플리케이션에서 트랜젝션 처리하는 방식에 따라서 choreography, orchestration 방식으로 구분됨 
    • 어플리케이션이 분리된 경우에는 각각의 local transaction만 처리
  • 각 어플리케이션에 대한 연속적인 트랜젝션에서 실패할 경우
    • 롤백 시 모든 값을 원상복구해야하는데, 관련한 롤백 처리 구현 -> 보상 트랜젝션이 준비되어 있음.
  • 데이터의 원자성을 보장하지 않지만 일관성을 보장 

내 할일만 잘하면 됨

장점

  • 탈결합: 서비스가 이벤트를 통해 통신하므로 독립적으로 발전할 수 있습니다.
  • 확장성: 각 서비스가 자체 트랜잭션을 관리하므로 수평적으로 확장이 가능합니다.
  • 회복력: 부분적인 실패와 보상을 허용하여 시스템 회복력을 높일 수 있습니다.

단점

  • 복잡성: 여러 서비스 간에 발생하는 이벤트 흐름을 추적하기 어려워지며, 트랜잭션 경로가 복잡해질 수 있습니다.
  • 최종 일관성(Eventual Consistency): 사가는 즉각적인 일관성을 보장하지 않으며, 시간이 지남에 따라 데이터가 일관성을 갖도록 합니다.
  • 오류 처리: 강력한 오류 처리 및 보상 로직 구현이 어려울 수 있습니다.

 

choreography-based saga

  1. 비동기 메시징:
    • 코레오그라피 사가는 각 서비스가 이벤트를 발행하고, 다른 서비스들이 이 이벤트를 구독하여 작업을 수행(상태를 조정)하는 방식으로 진행됩니다. 이 과정은 비동기로 이루어지며, 각 서비스는 서로의 상태를 직접 알지 않고 이벤트를 통해 통신합니다. 확장성이 높음
  2. 자율성:
    • 각 서비스는 스스로/독립적으로 행동하며, 다른 서비스와의 상호작용을 이벤트로만 처리합니다. 이러한 자율성 덕분에 서비스 간의 결합도가 낮아지고, 시스템의 유연성이 증가합니다.
  3. 상태 관리:
    • 코레오그라피 사가는 각 서비스가 자신의 상태를 관리합니다. 서비스는 다른 서비스의 상태에 의존하지 않고, 이벤트에 따라 자신의 행동을 결정합니다.
  4. 장애 복구:
    • 장애 발생 시 각 서비스는 발행된 이벤트에 따라 적절한 복구 작업을 수행할 수 있습니다. 실패한 작업을 롤백하기 위한 이벤트를 수신하고 처리할 수 있기 때문에 전체 시스템의 복원력을 높일 수 있습니다.

SAGA Choreography의 장점

  • 서비스 간의 느슨한 결합: 각 서비스는 자신의 트랜잭션만 신경 쓰며, 다른 서비스와는 이벤트를 통해 통신하므로 결합도가 낮습니다.
  • 확장성: 서비스들이 독립적으로 동작하므로, 새로운 서비스를 쉽게 추가할 수 있습니다.
  • 비동기 처리: 이벤트 기반 아키텍처이기 때문에 각 서비스는 비동기로 트랜잭션을 처리할 수 있습니다.

SAGA Choreography의 단점

  • 복잡성 증가: 여러 서비스 간에 발생하는 이벤트 흐름을 추적하기 어려워지며, 트랜잭션 경로가 복잡해질 수 있습니다.
  • 순환 참조 문제: 서비스 간의 이벤트 의존도가 높아지면 이벤트가 서로 꼬이는 순환 참조 문제가 발생할 수 있습니다.
  • 보상 트랜잭션 관리: 오류가 발생했을 때 롤백을 위한 보상 트랜잭션을 관리하는 로직이 복잡해질 수 있습니다.

 

SAGA의 코레오그래피 패턴을 안정적으로 운영하려면 여러 가지 고려 사항이 필요합니다. 코레오그래피에서는 각 서비스가 자신의 비즈니스 로직을 처리하면서 필요한 메시지를 전송하고, 실패 처리도 자체적으로 관리하는 방식입니다. 이 방식의 주요 장점은 중앙 관리자가 없다는 것입니다. 하지만 이로 인해 시스템의 복잡성도 높아지고, 신뢰성과 일관성 확보가 어려운 경우도 있습니다. 안정적인 운영을 위한 몇 가지 중요한 점들을 아래에 정리해 보겠습니다. (다운타임 고려)

1. 메시지 브로커의 안정성 확보

코레오그래피에서는 서비스 간의 통신을 비동기적으로 메시지를 통해 이루어지기 때문에, 메시지 브로커(예: Kafka, RabbitMQ)가 핵심적인 역할을 합니다. 이 시스템이 다운되거나 메시지 유실이 발생하면 전체 사가가 실패할 수 있습니다.

  • 메시지 영속성: 메시지 브로커는 재시작되더라도 메시지가 손실되지 않도록 보장 / 메시지를 영속적으로 저장할 수 있도록 설정
    • Kafka는 기본적으로 로그 기반 아키텍처를 사용하여 메시지를 디스크에 영속적으로 저장하므로 높은 내구성을 제공합니다. 메시지가 파티션에 기록되고, 각 파티션은 순서가 보장되며, 복제 설정을 통해 장애 발생 시에도 데이터 손실을 방지할 수 있습니다. 소비자는 이전에 처리된 메시지로부터 다시 시작할 수 있는 유연성을 제공합니다.
      • 스토리지: Kafka는 로그 기반 시스템으로, 메시지가 디스크에 지속적으로 기록되며, 지정된 기간 동안 또는 설정된 크기까지 보존됩니다.
    • 반면 RabbitMQ는 기본적으로 메모리 기반 큐로, 영속성을 위해 추가적인 설정이 필요합니다. 영속성을 위해 메시지를 디스크에 저장하면 성능 저하가 발생할 수 있습니다. 따라서 대규모 데이터 처리 및 내구성이 중요한 시스템에서는 Kafka가 더 적합할 수 있습니다.
  • 메세지 보장:
    • 래빗 - 메시지 확인 및 재처리:
      • 소비자가 메시지를 처리할 때 ACK(Acknowledge) 또는 NACK(Negative Acknowledge)를 사용하여 메시지의 처리가 성공적으로 완료되었는지 여부를 브로커에 알립니다.
        • ACK: 소비자가 메시지를 성공적으로 처리한 후에 ACK를 보내면, RabbitMQ는 해당 메시지를 큐에서 제거합니다.
        • NACK: 메시지 처리가 실패했을 경우 소비자는 NACK를 보내어 메시지를 다시 큐에 추가하거나 다른 큐로 이동할 수 있습니다. 이는 메시지 손실을 방지하는 데 도움을 줍니다.
      • 만약 소비자가 다운될 경우, 처리되지 않은 메시지는 브로커에 남아 있게 되어, 후에 재시도할 수 있습니다. 이로써 서비스가 다시 시작되면 미처리된 메시지를 재처리할 수 있습니다.
    • 카프카 - 오프셋 관리:
      • Kafka와 같은 로그 기반의 메시징 시스템을 사용하면, 각 소비자는 자신의 오프셋(offset)을 관리할 수 있습니다. 이를 통해 서비스가 재시작되더라도 마지막으로 처리한 메시지 이후의 메시지만 읽을 수 있어, 데이터 손실 없이 안정적으로 운영할 수 있습니다.
      • 오프셋 관리: 소비자가 메시지를 처리한 후 해당 메시지의 오프셋을 커밋하여 카프카 브로커에 저장합니다. 오프셋은 각 파티션에서 메시지가 몇 번째인지에 대한 정보입니다. 이 커밋은 소비자가 해당 메시지를 성공적으로 처리했음을 카프카에 알려주는 역할을 합니다.
        • 메시지 처리 완료 확인: 소비자는 커밋된 오프셋을 기준으로 메시지가 이미 처리되었음을 확인하고, 다음에 읽을 메시지를 결정합니다.
        • 메시지 중복 방지: 카프카는 마지막 커밋된 오프셋부터 메시지를 소비하므로, 오프셋을 잘 관리하면 메시지의 중복 처리를 방지할 수 있습니다.
  • 재시도 메커니즘: 메시지 브로커에서 메시지 처리가 실패했을 때  일정 횟수까지 재시도를 할 수 있는 메커니즘을 도입합니다. 재시도 횟수를 제한하거나, 실패한 메시지는 DLQ(Dead Letter Queue)에 보관하고, 해당 메시지에 대해 수동 또는 자동으로 복구할 수 있는 절차를 마련합니다.
    • RabbitMQ는 Dead Letter Queue(DLQ)와 Retry Queue를 활용하여 메시지가 처리되지 않았을 경우 이를 다시 시도하도록 할 수 있습니다.

2. 모니터링과 트레이싱 / 알람

코레오그래피 패턴에서는 각 서비스가 독립적으로 행동하기 때문에, 문제 발생 시 전체 시스템에서 어떤 서비스가 문제를 일으켰는지 추적하는 것이 중요합니다. 메시지 처리 지연, 실패율 등을 관찰하고, 문제가 발생할 경우 경고를 받을 수 있습니다.

  • 분산 추적 시스템: 예를 들어, OpenTelemetry, Zipkin, Jaeger 등을 사용하여 분산 트레이싱을 설정합니다. 이를 통해 각 서비스에서 발생하는 요청과 응답을 추적하고, 실패나 지연이 발생한 지점을 쉽게 파악할 수 있습니다.
  • 로그 집합 시스템: 각 서비스에서 발생하는 로그를 통합하여 분석할 수 있는 시스템(예: ELK Stack, Prometheus, Grafana)을 구축합니다. 서비스 간 메시지 흐름을 추적하고, 오류를 신속하게 발견할 수 있도록 합니다.

3. 상태 관리와 보상 트랜잭션

코레오그래피 패턴에서는 각 서비스가 자신만의 상태를 관리하며, 실패 시 보상 트랜잭션을 처리해야 합니다. 보상 트랜잭션이란 이전 단계에서 발생한 변경을 되돌리는 작업을 의미합니다.

  • 보상 트랜잭션 설계: 각 서비스는 실패가 발생할 경우 보상 작업을 자동으로 실행해야 합니다. 예를 들어, 결제 처리 서비스에서 결제가 실패하면 이전에 발생한 예약을 취소하는 등의 처리를 해야 합니다.
  • 보상 트랜잭션의 신뢰성: 보상 트랜잭션이 실패하지 않도록, 처리되는 모든 작업은 idempotent(멱등성; 중복 실행 시 동일한 결과를 보장하는) 방식이어야 합니다. 이를 통해 여러 번 재시도가 이루어져도 시스템이 일관성 있게 동작할 수 있습니다.
  • 보상 트랜잭션의 회복: 보상 트랜잭션 역시 실패할 수 있기 때문에, 보상 작업이 실패하면 이를 다시 시도하거나 수동으로 복구할 수 있는 시스템을 마련해야 합니다.

4. 데이터 일관성 확보

코레오그래피 패턴에서는 이벤트 기반으로 서비스가 통신하므로, 데이터 일관성 문제를 해결하는 것이 중요합니다. 서비스 간의 데이터 변경이 일관되게 이루어지도록 해야 합니다.

  • 최종 일관성 보장: 코레오그래피 패턴은 강력한 일관성(ACID) 대신 최종 일관성(eventual consistency)을 제공합니다. 즉, 시간이 조금 걸릴 수 있지만 모든 서비스가 일관된 상태에 도달하도록 설계해야 합니다. 이를 위해 서비스들이 동일한 이벤트를 처리하는 순서를 보장하거나, 가능한 한 빠르게 일관된 상태로 수렴할 수 있도록 해야 합니다.

5. SLA(서비스 수준 계약, Service Level Agreement)와 트랜잭션의 타임아웃

서비스들이 비동기적으로 처리되기 때문에 각 단계의 응답 시간이 지연될 수 있습니다. 각 서비스가 적절한 시간 내에 작업을 완료하지 않으면 전체 SAGA가 실패할 수 있습니다.

  • SLA 설정: 각 서비스의 작업에 대해 적절한 SLA(Service Level Agreement)를 설정하고, 이 SLA를 기준으로 타임아웃을 설정하여 시스템이 적절한 시간 내에 작업을 완료할 수 있도록 합니다.
    • ex. 모든 API 요청에 대한 응답 시간은 200ms 이내로 처리
      • 응답 시간의 95%는 200ms를 초과하지 않아야 
      • 최대 응답 시간은 500ms를 초과할 수 없음
  • 타임아웃 처리: 각 서비스가 SLA를 지키지 않으면, 적절한 처리(예: 오류 로그 기록, 알림 발송, 재시도 등)를 통해 전체 트랜잭션을 관리합니다.

6. 이벤트 소싱

  • 모든 상태 변경을 이벤트로 기록하고 이를 통해 현재 상태를 재구성할 수 있는 이벤트 소싱(Event Sourcing) 패턴을 활용할 수 있습니다. 이렇게 하면 각 서비스가 발생한 이벤트를 기반으로 자신의 상태를 재구성할 수 있어, 데이터의 일관성을 유지하는 데 도움이 됩니다.
    •  

orchestration-based saga

  • 중앙 통제자: 중앙 조정자(orchestrator) 역할을 하는 서비스가 각 서비스의 트랜잭션을 관리합니다. 이 조정자는 트랜잭션 단계를 순차적으로 실행하고, 필요한 경우 보상 작업을 수행합니다.
  • 흐름 제어: 오케스트레이터는 각 서비스 호출의 순서를 정의하고, 에러 처리나 롤백도 중앙에서 관리합니다. 동기/비동기 모두 가능
  • 장점:
    • 단순한 흐름 제어: 중앙에서 모든 것을 관리하기 때문에 트랜잭션 흐름이 명확합니다.
    • 에러 처리 및 롤백이 통합되어 관리되므로 일관성을 유지하기 쉽습니다.; 오류 처리 쉬움
  • 단점:
    • Single Point of Failure: 중앙 통제자가 다운될 경우 전체 트랜잭션이 영향을 받습니다.
    • 확장성 문제: 모든 트랜잭션을 중앙에서 관리하기 때문에 트랜잭션 수가 많아질수록 부담이 커질 수 있습니다.

1. 동기식 오케스트레이션

  • 설명: 모든 서비스 호출이 순차적으로 진행되며, 각 서비스 호출이 완료될 때까지 대기합니다. 즉, 오케스트레이터가 각 서비스에 요청을 보내고 응답을 받을 때까지 기다립니다.
  • 장점:
    • 단순한 흐름: 트랜잭션의 흐름이 명확하고 예측 가능하여 디버깅과 모니터링이 용이합니다.
    • 즉각적인 오류 처리: 오류가 발생하면 즉시 대응할 수 있으며, 다음 단계로 진행하기 전에 에러 처리를 수행할 수 있습니다.
  • 단점:
    • 지연: 각 서비스 호출이 완료될 때까지 기다려야 하므로 전체 트랜잭션의 실행 시간이 늘어날 수 있습니다.
    • 스케일 문제: 동기식 호출이 많아지면 시스템의 성능이 저하될 수 있습니다.

2. 비동기식 오케스트레이션

  • 설명: 서비스 호출이 비동기적으로 이루어지며, 오케스트레이터는 각 서비스에 요청을 보내고 응답을 기다리지 않고 다음 작업을 진행합니다. 일반적으로 메시지 큐를 사용하여 서비스 간의 통신을 처리합니다.
  • 장점:
    • 성능 향상: 각 서비스가 독립적으로 작동하므로 동시에 여러 요청을 처리할 수 있어 성능이 향상됩니다.
    • 유연성: 서비스 간의 느슨한 결합을 유지할 수 있으며, 서비스가 독립적으로 확장될 수 있습니다.
  • 단점:
    • 복잡성: 비동기 통신은 흐름을 이해하고 디버깅하기 더 어렵게 만들 수 있습니다.
    • 오류 처리: 각 서비스가 비동기적으로 작동하므로 에러 발생 시 처리하기가 더 복잡해질 수 있습니다.

 

오케스트레이터는 복잡한 프로세스나 워크플로우를 관리하고 조정하는 시스템입니다. 마이크로서비스 아키텍처에서는 다양한 서비스 간의 상호작용을 관리하는 데 사용됩니다. 오케스트레이터는 일반적으로 다음과 같은 역할을 합니다:

  • 서비스 간의 호출 순서 및 의존성을 관리
  • 오류 처리 및 재시도 로직 구현
  • 상태를 모니터링하고 필요한 경우 알림 전송

오케스트레이터는 여러 형태로 구현될 수 있습니다:

  1. 전용 프로그램:
    • Apache Airflow, Camunda, Temporal과 같은 오케스트레이션 도구는 비즈니스 프로세스를 자동화하고 조정하는 데 사용됩니다. 이들 도구는 복잡한 워크플로우를 설계하고 실행하는 데 필요한 기능을 제공합니다.
  2. 메시지 브로커:
    • Kafka와 같은 시스템은 메시지를 전달하고 여러 서비스 간의 통신을 조정할 수 있지만, 기본적으로는 오케스트레이션 기능을 제공하지 않습니다. Kafka는 주로 데이터 스트리밍 및 이벤트 기반 아키텍처에 사용되며, 오케스트레이션과는 다릅니다. 하지만 오케스트레이터와 메시지 브로커를 함께 사용하여 전체 시스템을 구성하는 것이 일반적..
  3. 서비스 메쉬:
    • Istio와 같은 서비스 메쉬는 마이크로서비스 간의 통신을 관리하고 보안, 트래픽 관리, 모니터링 등의 기능을 제공합니다. 이러한 솔루션도 오케스트레이션의 일종으로 간주될 수 있습니다.

 


2 phase commit

2PC(2-Phase Commit, 2단계 커밋)는 분산 데이터베이스 시스템에서 트랜잭션을 안전하게 커밋하기 위해 사용하는 프로토콜입니다. 이 프로토콜은 여러 데이터베이스 또는 서비스가 관련된 트랜잭션을 일관성 있게 처리하기 위해 설계되었습니다.

2-Phase Commit 동작 단계

  1. Prepare 단계:
    • **트랜잭션 코디네이터(Coordinator)**가 모든 참여 노드(또는 서비스)에 "Prepare" 메시지를 전송합니다.
    • 각 참여자는 트랜잭션을 수행할 준비가 되면 로컬로 락을 설정하고 "Yes"(준비 완료) 응답을 보냅니다.
    • 만약 어떤 노드가 준비되지 않거나 실패하면 "No"(준비 실패) 응답을 보냅니다.
  2. Commit 단계:
    • 코디네이터가 모든 노드로부터 "Yes" 응답을 받으면, 각 노드에 **트랜잭션을 커밋(commit)**하도록 지시하고 트랜잭션이 완료됩니다.
    • 만약 어느 하나의 노드라도 "No" 응답을 보낸 경우, 트랜잭션을 전체적으로 롤백합니다.
    • 커밋이나 롤백 완료 후 각 노드는 락을 해제합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;

@Service
public class InventoryService {

    @Autowired
    private DataSource warehouse1DataSource;

    @Autowired
    private DataSource warehouse2DataSource;

    @Transactional
    public void updateInventory(String productId, int quantity) throws Exception {
        try (Connection conn1 = warehouse1DataSource.getConnection();
             Connection conn2 = warehouse2DataSource.getConnection()) {

            String updateWarehouse1 = "UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?"; 
            try (PreparedStatement ps1 = conn1.prepareStatement(updateWarehouse1)) {
                ps1.setInt(1, quantity / 2);
                ps1.setString(2, productId);
                ps1.executeUpdate();
            }

            String updateWarehouse2 = "UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?";
            try (PreparedStatement ps2 = conn2.prepareStatement(updateWarehouse2)) {
                ps2.setInt(1, quantity / 2);
                ps2.setString(2, productId);
                ps2.executeUpdate();
            }
        } catch (Exception e) {
            throw new RuntimeException("Inventory update failed, rolling back transaction", e);
        }
    }
}

위 코드에서 2PC의 Prepare 단계Commit 단계는 트랜잭션 매니저(JtaTransactionManager)에 의해 관리됩니다. 코드 자체에는 Prepare와 Commit이라는 명시적인 단계가 보이지 않지만, JtaTransactionManager가 내부적으로 이 과정을 수행합니다.


MySQL에서 UPDATE 쿼리를 실행할 때, InnoDB는 기본적으로 **행 잠금(Row Lock)**을 사용하여 UPDATE 대상이 되는 행을 잠급니다. 다른 트랜잭션에서 읽을 수는 있지만, 실제로 쓰기를 시도하려면 트랜잭션이 완료될 때까지 기다려야 하므로 일시적인 충돌은 방지됩니다. 하지만 다수의 트랜잭션에서 동시에 동일한 데이터를 수정하려고 하면 **경쟁 조건(Race Condition)**이 발생할 수 있습니다.

INSERT 쿼리에서 InnoDB는 기본적으로 행 잠금을 사용하지 않습니다. 즉, INSERT가 실행될 때는 행에 락을 걸지 않으며, 다른 트랜잭션은 동시에 다른 행을 삽입할 수 있습니다.

 

  • auto_increment 필드를 사용하는 경우, InnoDB는 자동 증가 값을 생성할 때 해당 레코드에 잠금을 걸 수 있습니다. 이 값이 충돌하지 않도록 내부적으로 관리되기 때문입니다.
  • 예를 들어, 여러 트랜잭션이 동시에 INSERT를 시도할 때, 자동 증가 값에 대한 충돌을 피하기 위해 자동 증가 번호를 추적하기 위한 락이 필요합니다. 이 경우 다른 트랜잭션은 번호가 할당될 때까지 대기할 수 있습니다.

 

1. Prepare 단계

Prepare 단계는 트랜잭션 관리자가 각 데이터 소스에 트랜잭션을 시작하고 작업을 준비시키는 과정입니다. 여기서 중요한 역할을 하는 부분은 @Transactional 애너테이션입니다. 이 애너테이션을 통해 트랜잭션이 시작되면 다음과 같은 작업들이 순서대로 진행됩니다.

  • 트랜잭션 매니저는 각각의 데이터 소스에 대해 트랜잭션을 시작합니다.
  • updateWarehouse1와 updateWarehouse2 쿼리를 실행하여 각 데이터베이스에 변경 사항을 기록합니다.
  • Prepare 단계에서는 실제 커밋을 수행하지 않고, 각 데이터베이스에 잠금(Lock)을 통해 데이터의 일관성을 보장합니다.

만약 이 단계에서 에러가 발생하면 JtaTransactionManager가 rollback을 호출하여 전체 트랜잭션이 취소됩니다.

2. Commit 단계

Commit 단계는 모든 준비가 완료된 후, 각 데이터 소스에 대해 트랜잭션을 실제로 커밋하는 과정입니다. 이 단계에서는 다음 작업들이 수행됩니다.

  • 트랜잭션 매니저가 각 데이터 소스에 commit 명령을 내리고, 모든 변경 사항을 데이터베이스에 영구적으로 반영합니다.
  • 각 데이터베이스가 성공적으로 커밋되면, 트랜잭션이 완료됩니다.
  • 만약 이 과정에서 하나라도 실패하면 rollback이 호출되어 모든 데이터베이스의 작업이 원상복구됩니다.

정리하면:

  • @Transactional 애너테이션을 통해 시작되는 트랜잭션이 Prepare 단계에서 두 데이터베이스에 모두 준비된 상태로 변경 작업을 적용한 후 잠금을 유지합니다.
  • 트랜잭션 매니저가 커밋을 호출하면서, 실제 변경 사항이 모든 데이터베이스에 커밋되는 것이 Commit 단계입니다.

2-Phase Commit에서의 락(Lock)과 문제점

락의 사용:

  • 각 참여 노드는 "Prepare" 단계에서 트랜잭션에 관련된 자원을 락합니다. 이 락은 트랜잭션의 커밋 또는 롤백이 완료될 때까지 유지되어, 다른 트랜잭션이 해당 자원에 접근하지 못하게 합니다.
  • 이 락은 데이터 일관성을 유지하는 데 필수적이지만, 락을 길게 유지하면 성능 저하가 발생할 수 있습니다.

문제점:

  • 블로킹(Blocking): 한 노드가 준비 상태에서 응답을 보내지 못하거나 장애가 발생하면, 코디네이터는 트랜잭션의 커밋 또는 롤백을 기다려야 하므로 전체 트랜잭션이 중단될 수 있습니다.
  • 확장성 문제: 트랜잭션이 많아지고 락을 획득하는 시간이 길어질수록 시스템의 성능이 저하됩니다.
  • 고립된 락: 참여자가 죽거나 네트워크 분리가 발생하면 락이 해제되지 않는 문제가 발생할 수 있습니다.

이러한 문제로 인해, 2PC는 락 비용이 크고 고가용성을 요구하는 시스템에는 적합하지 않으며, 대신 SAGA 패턴이나 **3-Phase Commit(3PC)**과 같은 대안이 사용되기도 합니다.


실패 가능 시나리오

1. Prepare 단계에서 참가자 실패

  • 상황: 코디네이터가 각 참가자에게 Prepare 메시지를 보내고 승인을 기다리는 동안, 한 참가자가 응답하지 않거나 실패하는 경우입니다.
  • 처리 방식:
    • 대기 하다가
    • 타임아웃 설정(권장): 참가자가 Prepare 단계에서 정해진 시간 내에 응답하지 않으면, 코디네이터는 해당 참가자를 실패로 간주합니다.
    • 롤백 결정: 코디네이터는 전체 트랜잭션을 롤백하기로 결정하고 모든 응답한 참가자에게 Rollback 명령을 보냅니다.
    • 일관성 보장: 응답하지 않은 참가자 역시, 복구 시 자신의 트랜잭션을 롤백하는 규칙을 따릅니다.

2. Commit 단계에서 참가자 실패

  • 상황: 모든 참가자가 Prepare 요청에 승인했으나, Commit 명령을 기다리는 도중에 일부 참가자가 실패합니다.
  • 처리 방식:
    • Commit 의무: Prepare에서 Commit을 승인한 참가자는 이후 반드시 Commit을 완료해야 합니다.
    • 복구 프로세스: 실패한 참가자는 복구되었을 때 로그를 확인하여 Commit 또는 Rollback을 수행합니다. 참가자는 Prepare 단계에서 승인 후 실패하면, 복구 후 반드시 Commit을 실행해야 일관성이 유지됩니다.
      • 코디네이터가 '대기'하는 것이 아닌, 참가자가 로그를 확인하는 방식: 코디네이터는 더 이상 해당 트랜잭션을 대기하지 않고 종료

3.  Prepare 단계에서의 코디네이터 실패:

    • 대기 상태 유지: 참가자는 Commit 또는 Rollback 명령을 받을 때까지 대기합니다.
      • 코디네이터가 Prepare 메시지를 보낸 후 실패하면, 참가자들은 각자의 상태를 Prepared로 유지하며 Commit 또는 Rollback 명령을 기다립니다.
      • 이 시점에서는 코디네이터의 복구나 교체 없이는 참가자들이 개별적으로 Commit이나 Rollback을 결정하지 못하므로, 참가자들은 일시적으로 대기 상태에 들어갑니다.
    • 타임아웃 및 복구: 코디네이터가 복구되면 로그를 확인해 Prepare 상태를 마친 참가자에게 Commit 또는 Rollback을 지시합니다. 새로운 코디네이터가 지정되거나 기존 코디네이터가 복구되기를 기다리며 재시도를 합니다.
      • 재시도 메커니즘: 참가자가 코디네이터의 상태를 감지할 수 있도록 재시도 메커니즘을 적용할 수 있습니다. 코디네이터가 다시 살아날 경우 트랜잭션을 다시 시도할 수 있습니다.  
      • 참가자들은 일정 시간 동안 코디네이터의 응답을 기다리도록 타임아웃을 설정할 수 있습니다. 타임아웃이 발생하면 참가자는 문제가 생겼음을 인식하고 롤백 프로세스를 시작하게 됩니다.
    • 코디네이터가 복구되거나 새로운 코디네이터가 지정되면 모든 참가자들의 상태 로그를 검토하여 트랜잭션 상태(Commit 또는 Rollback)를 확인합니다.

4. Commit 단계에서의 코디네이터 실패:

  1. Commit 명령을 받은 후 코디네이터가 실패한 경우:
    • 코디네이터가 모든 참가자에게 Commit 명령을 전달한 후에 실패했다면, 대부분의 참가자는 이미 Commit을 완료했을 것입니다.
    • 참가자는 트랜잭션 로그에 Commit 완료 상태를 기록하여 코디네이터가 없는 상태에서도 트랜잭션 완료 상태를 기억할 수 있습니다.
    • 새로운 코디네이터가 복구되면 참가자들의 로그 상태를 조회 일관성을 검증하고, 트랜잭션이 Commit으로 완료되었는지 확인합니다.
  2. Commit 명령이 일부 참가자에게 전달되지 않은 경우:
    • 코디네이터가 일부 참가자에게 Commit 명령을 보내기 전에 실패하면, 이 경우 일부 참가자만 Commit을 수행하고 나머지 참가자는 Prepared 상태로 남아 있을 수 있습니다.
    • 이때 참가자들은 코디네이터의 복구나 새로운 코디네이터가 지정될 때까지 로그를 통해 자신의 상태를 유지하며 대기하게 됩니다.
    • 새로운 코디네이터는 각 참가자의 상태를 확인하여, 이미 Commit을 완료한 참가자가 있으면 전체 트랜잭션을 Commit하도록 모든 참가자에게 지시합니다.
  3. 최종적으로 Commit을 보장하기 위한 절차:
    • 새로운 코디네이터가 지정되면, 각 참가자에게 현재 트랜잭션 상태를 질의하고 이를 바탕으로 트랜잭션의 일관된 완료 상태를 결정합니다.
    • 만약 일부만 Commit을 완료한 상태로 확인되면, 모든 참가자에게 Commit 명령을 다시 전달하여 트랜잭션을 완결시킵니다.

이 과정을 통해 트랜잭션은 무조건 동일한 최종 상태(Commit 또는 Rollback)로 마무리되며, 코디네이터가 Commit 단계에서 실패하더라도 로그와 상태 확인을 통해 일관성을 보장합니다.

2PC의 장점

  1. 트랜잭션 일관성 보장:
    • 모든 참여 시스템이 트랜잭션을 동시에 커밋하거나 롤백하기 때문에 데이터 일관성을 유지할 수 있습니다. 이는 금융 시스템이나 다른 중요한 데이터가 관련된 애플리케이션에서 필수적입니다.
  2. 구현의 단순성:
    • 두 가지 단계(Prepare 및 Commit)로 이루어져 있어 구현이 상대적으로 단순합니다. 트랜잭션 관리가 필요한 경우 쉽게 적용할 수 있습니다.

2PC의 단점

  1. 코디네이터의 단일 실패:
    • 코디네이터가 실패하면 모든 참가자가 해당 트랜잭션에 대해 무기한으로 대기하게 되어 시스템이 불안정해집니다. 코디네이터 장애 복구가 어려워 전체 트랜잭션에 영향을 줄 수 있습니다.
  2. 참가자의 무한 대기 문제:
    • Prepare 단계에서 승인 후 // 코디네이터가 Commit이나 Rollback 명령을 전달하지 않으면 참가자들은 대기 상태에 빠집니다. 이는 네트워크 장애나 코디네이터 장애로 인한 무한 대기 문제로 이어질 수 있습니다.
  3. 락 유효성 문제
    • 2PC 프로토콜에서는 참가자들이 트랜잭션 상태를 준비하는 동안 데이터에 락을 걸고 대기하므로, 데이터베이스 자원의 락이 장기간 유지될 수 있습니다.
    • 트랜잭션이 길어지면 데이터베이스 자원에 대한 경합이 심해져, 다른 트랜잭션들이 성능 저하 겪을 수 있습니다.
  4. 네트워크 지연 문제:
    • 코디네이터와 각 참가자 간의 통신에서 지연이 발생할 경우 트랜잭션 완료까지 시간이 길어집니다. 이로 인해 자원이 오랫동안 잠금 상태로 유지되어 성능 저하가 발생할 수 있습니다.
  5. 네트워크 파티션 문제:
    • 2PC는 네트워크 파티션이 발생할 경우 각 노드가 올바른 결정을 내리기 어려운 문제가 있습니다. 네트워크가 복구될 때까지 모든 시스템이 일관성을 보장할 방법이 없어 데이터 일관성 유지가 어려워질 수 있습니다.
  6. 확장성 제한:
    • 2PC동기화 방식으로 작동하므로 확장성(scalability) 문제에 직면할 수 있습니다. 분산 시스템에서 많은 참가자가 동시에 참여하는 경우, 각 단계에서 지연이 누적되기 때문에 트랜잭션의 성능 저하가 발생할 수 있으며, 네트워크 지연, 대기 시간, 동기화 비용 등이 시스템 성능에 큰 영향을 미칠 수 있습니다.

로그 활용과 복구 절차

  • 참가자와 코디네이터의 로그 기록: 모든 참여자는 트랜잭션 진행 상황을 로그에 기록하여, 장애 발생 시 트랜잭션 상태를 복구하는 데 사용합니다. 예를 들어, Prepare 승인 여부, Commit 및 Rollback 상태 등을 기록합니다.
  • 복구 절차:
    • 코디네이터 복구: 코디네이터는 복구 시 자신의 로그를 확인하여 아직 완료되지 않은 트랜잭션을 찾고, 참가자에게 나머지 명령을 수행하도록 합니다.
    • 참가자 복구: 참가자는 복구 시, 마지막 로그 상태에 따라 트랜잭션을 Commit 또는 Rollback합니다.

동기화 문제와 대안

  • 2PC는 장애 시 동기화 문제로 인해 성능 저하가 발생할 수 있습니다. 이런 상황을 최소화하기 위해 많은 분산 시스템에서는 SAGA 패턴과 같은 비동기 트랜잭션 관리 방식을 선택하기도 합니다.
  • SAGA 패턴은 복구(데이터 일관성이 맞던 시점으로 돌아가) 및 보상 트랜잭션(되돌리기; 롤백)을 사용하여, 전체 시스템을 잠그지 않고 트랜잭션을 처리하는 대안을 제공합니다.
  • 2-Phase Commit (2PC)은 일반적으로 동기적으로 동작합니다. 즉, 모든 참가자가 각 단계에서 응답을 제공하기 전까지 다음 단계로 진행하지 않는 구조입니다.
  1. Prepare 단계의 동기성:
    • 코디네이터는 각 참가자에게 Prepare 요청을 보내고, 모든 참가자가 성공을 응답할 때까지 대기합니다. 모든 참가자가 준비 완료 상태임을 확인할 때까지 다음 단계인 Commit으로 넘어가지 않습니다.
    • 모든 응답을 기다리기 때문에 각 참가자는 해당 트랜잭션에 대해 락을 걸고 대기하며, 이는 다른 트랜잭션의 접근을 막아 동시성에 영향을 미칩니다.
  2. Commit/Abort 단계의 동기성:
    • 모든 참가자가 Prepare에 응답한 후, 코디네이터는 각 참가자에게 Commit 또는 Abort 명령을 동기적으로 보냅니다.
    • 코디네이터는 모든 참가자의 Commit 응답이 도착할 때까지 최종 완료를 확인하지 않으며, 트랜잭션이 완전히 완료된 것을 보장하기 위해 이 과정을 동기적으로 처리합니다.

동기적 특성으로 인한 제약사항

  • 성능 저하: 모든 참여자의 응답을 기다리는 과정에서 응답 지연이 발생할 수 있습니다.
  • 지연 시간 증가: 네트워크 지연이나 참가자의 성능 문제로 인해 2PC의 완료 시간이 길어질 수 있습니다.
  • 확장성 제한: 참가자가 늘어날수록 동기적으로 기다려야 하는 응답이 많아져 확장성이 제한됩니다.
  • 고가용성 문제: 코디네이터나 참가자가 중간에 실패할 경우 전체 트랜잭션이 중단되며, 동기성으로 인해 대기 상태에 빠지게 됩니다.

코디네이터는 누가 어떻게?

1. 트랜잭션 코디네이터 역할

  • 트랜잭션 시작: 트랜잭션을 시작하고, 모든 참여 노드(예: 여러 DB 인스턴스, 서비스 등)에게 트랜잭션 참여 요청을 전달합니다.
  • Prepare 단계 관리: 모든 노드에 Prepare 요청을 보내고, 각 노드가 트랜잭션을 커밋할 준비가 되었는지 확인합니다.
  • Commit 또는 Rollback 결정: 모든 노드가 "Prepare 성공" 응답을 반환하면 Commit을, 실패 응답이 있으면 Rollback을 지시합니다.
  • 상태 관리 및 오류 처리: 네트워크 오류나 장애가 발생하면 트랜잭션을 적절하게 중단시키고, 필요 시 롤백합니다.

2. 트랜잭션 코디네이터를 수행하는 시스템

  • 데이터베이스 관리 시스템(DBMS):
    • 많은 DBMS(예: Oracle, PostgreSQL, MySQL 등)에서 자체적으로 2PC를 지원하는 경우 DB 인스턴스 자체가 트랜잭션 코디네이터 역할을 할 수 있습니다.
    • 복수의 DB 노드가 분산 트랜잭션에 참여해야 할 때, DBMS가 코디네이터가 되어 트랜잭션을 조율합니다.
  • 트랜잭션 관리 시스템(TMS):
    • **Java EE 서버(JBoss, WebSphere)**와 같은 엔터프라이즈 애플리케이션 서버는 **JTA(Java Transaction API)**를 통해 트랜잭션을 관리하며, 코디네이터 역할을 할 수 있습니다.
    • JTA는 XA 프로토콜을 사용해 DBMS와 메시지 큐 시스템이 동시에 참여하는 분산 트랜잭션을 조율할 수 있습니다.
  • 클라우드 및 분산 시스템:
    • 클라우드 환경에서는 Google Spanner, Amazon Aurora, Microsoft Azure SQL Database 같은 관리형 데이터베이스 서비스가 트랜잭션 코디네이터 기능을 제공합니다.
    • Kubernetes와 같은 환경에서는 외부 트랜잭션 관리 툴을 통해 분산 트랜잭션을 관리할 수도 있습니다.

3. 트랜잭션 코디네이터의 필요성과 한계

  • 2PC 코디네이터는 분산 트랜잭션에서 데이터 일관성을 보장하는 중요한 역할을 하지만, 네트워크 지연데드락 위험 때문에 트랜잭션 수행 속도가 느려질 수 있습니다.
  • 최근에는 이러한 한계를 극복하기 위해 SAGA 패턴과 같은 분산 트랜잭션 대안이 각광받고 있습니다.

3PC의 단계와 개선 내용

  1. Can Commit 단계:
    • 코디네이터가 트랜잭션을 수행할 준비가 되었는지 모든 참여자에게 묻습니다.
    • 각 참여자는 승인 가능 여부를 응답하고, 타임아웃이 지나면 No로 간주됩니다.
    이 단계는 기존 2PC와 비슷하지만, 참가자가 제한 시간 내 응답을 못하면 자동으로 No로 처리되므로 무기한 대기가 줄어듭니다.
  2. Pre-Commit 단계 (3PC에서 추가된 단계):
    • 코디네이터가 모든 참여자로부터 Yes 응답을 받으면, Pre-Commit 요청을 보냅니다.
    • 이때 각 참가자는 트랜잭션을 일시적으로 저장하지만 실제 커밋하지 않고 대기합니다.
    • 이 과정에서도 타임아웃이 강제되어 응답이 지연되면 트랜잭션을 롤백할 수 있습니다.
    이 단계가 추가됨으로써 참여자들은 코디네이터나 다른 참가자가 실패하더라도 롤백으로 일관성을 유지할 수 있습니다.
  3. Commit 단계:
    • 코디네이터가 최종 커밋 명령을 보내며, 모든 참가자는 타임아웃 내에 커밋을 완료합니다.
    • 코디네이터가 실패하더라도 참가자들이 Pre-Commit 단계를 통해 트랜잭션을 커밋하거나 롤백할 수 있는 상태가 되므로, 무한 대기를 피하고 일관성을 유지할 수 있습니다.

3PC의 주요 개선점: 복구 및 안정성

  • 무한 대기 방지: 각 단계에 타임아웃을 설정하여 코디네이터 또는 참여자가 응답하지 않을 경우에도 결정할 수 있습니다.
  • 비동기적 장애 복구 가능: Pre-Commit 단계를 통해 참여자들이 커밋을 확정하기 전 준비 상태에서 롤백할 수 있어, 코디네이터가 중간에 실패하더라도 참여자들은 대기 없이 결정할 수 있습니다.
  • 시스템 신뢰성 증가: 네트워크 장애나 일시적인 서버 장애가 있어도 참여자들이 독립적으로 커밋/롤백을 결정할 수 있어, 전체 시스템의 신뢰성을 높입니다.

3PC(3-Phase Commit)는 기본적으로 동기 방식의 트랜잭션 프로토콜입니다. 하지만 비동기 처리와 관련된 개념이 몇 가지 존재합니다. 

동기 방식

  • 각 단계에서의 대기: 3PC는 각 단계(Prepare, Pre-Commit, Commit)에서 코디네이터와 참가자 간의 메시지 전송이 이루어지며, 코디네이터는 각 참가자로부터 응답을 기다립니다. 이 과정에서 모든 참가자가 응답할 때까지 대기하는 방식이므로 동기적입니다.
  • 모든 참여자와의 동기화: 트랜잭션의 일관성을 유지하기 위해 코디네이터는 모든 참가자로부터 동기적으로 응답을 받아야 하며, 이를 통해 트랜잭션을 진행합니다.

비동기 처리의 개념

  • 타임아웃 및 복구: 3PC에서는 각 단계에 타임아웃이 설정되어 있어, 특정 참가자가 응답하지 않으면 자동으로 트랜잭션을 롤백할 수 있습니다. 이로 인해 비동기적 장애 복구가 가능해지며, 대기하지 않고 다른 작업을 수행할 수 있는 가능성이 생깁니다.
  • Pre-Commit 단계: Pre-Commit 단계에서 각 참가자는 트랜잭션을 준비하지만, 실제로 커밋하기 전까지는 대기합니다. 이 상태에서 참가자는 코디네이터의 요청이 없더라도 자신이 결정할 수 있는 상태가 되므로, 이 점에서 비동기적인 특성을 가질 수 있습니다.

2PC에서의 상태 확인:

  • 2PC에서도 참가자는 로그를 통해 자신의 상태를 확인할 수 있습니다. 예를 들어, Prepare 단계에서 참가자가 Vote to Commit을 보낸 후, 이 정보는 참가자의 로그에 기록됩니다.
  • 그러나 코디네이터가 실패한 경우, 참가자는 여전히 트랜잭션이 완료되었는지, 롤백되었는지 알 수 없습니다. 코디네이터가 실패하면 참가자는 대기해야 하고, 자신의 상태만으로는 트랜잭션을 커밋할지 롤백할지 결정을 내릴 수 없습니다.
    • 참가자는 코디네이터의 결정을 기다려야 하기 때문입니다.
    • 2PC에서는 코디네이터가 트랜잭션을 완료할지 롤백할지를 최종적으로 결정합니다.

3PC에서의 복구 가능성:

  • 3PCPreCommit 단계를 추가로 도입하여, 참가자가 자신의 상태를 확인하고, 트랜잭션을 진행할 수 있는 준비 상태에 있다는 사실을 명확히 알 수 있게 합니다.
  • 코디네이터가 실패하면, 참가자는 PreCommit 상태에서 자기 상태를 확인하고 복구된 코디네이터가 트랜잭션을 커밋할지 롤백할지를 최종적으로 결정할 수 있도록 대기하는 상태로 넘어갑니다.
  • 참가자는 자신이 PreCommit 상태라는 정보를 가지고 있으며, 복구 후 트랜잭션을 진행할 준비가 되었다는 것을 알지만, 최종적인 결정은 코디네이터가 내리기 때문에 대기하는 것입니다.

결론:

  • 2PC에서는 참가자가 자신의 상태를 로그에서 확인할 수 있지만, 코디네이터가 실패한 경우 참가자는 대기해야 하며 자기 혼자서 결정을 내리기 어렵습니다.
  • 3PC에서는 PreCommit 상태로 참가자가 자기 준비 상태를 확인할 수 있지만, 결정은 여전히 코디네이터가 내리게 됩니다. 다만, PreCommit 상태로 명확한 정보를 제공하여, 복구 후 결정을 더 빨리 내릴 수 있는 장점이 있습니다.
728x90
반응형
반응형

환경: springboot2.7.6

 

로컬에서 개발할 때, 그리고 운영 환경으로 배포할 때 내용물에 따라 설정파일을 분리할 수 있으며, 환경에 따라 다르게 가져가야 한다.

-- 소스 실행
mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=production

-- jar 로 빌드해서 배포
java -Dspring.profiles.active=production service.jar

 

설정파일은 파일명 자체를 여러가지로 둘 수도 있고, 그 안에서 환경(profile)을 줄 수도 있다.

 

자바 소스 안에서 프로파일을 분기할 수도 있다.

728x90
반응형
반응형

환경: windows11, springboot2.7.6, java17

 

springboot 프로젝트인 catalog service 를 도커에 올려본다.

 

1. pom.xml 경로에 Dockerfile 생성

FROM openjdk:17-ea-slim-buster
VOLUME /tmp
COPY target/catalog-service-1.0.jar catalog-service.jar
ENTRYPOINT ["java", "-jar", "catalog-service.jar"]

pom.xml 파일 확인하여 jar가 위 이름으로 빌드되는지 확인 필요

 

2. 도커 이미지 생성

mvn clean compile package -DskipTests=true
docker build -t haileyjhbang/catalog-service:1.0 .  //도커이미지생성

 

3. 도커 이미지 -> repository 푸시

docker push haileyjhbang/catalog-service:1.0

 

4. 도커 실행

실행 시 사용 중인 외부 접속 정보가 있으면 아래처럼 전달하는 방법 사용

소스&application.yml 파일 내/외부 꼼곰히 확인 필요

해당 부분 수정 필

docker run -d --network ecommerce-network --name catalog-service -e "eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/" -e "logging.file=/api-logs/catalog-test.log" haileyjhbang/catalog-service:1.0

 

docker network insepect ecommerce-network
localhost:8761

성공적..

728x90
반응형

+ Recent posts