728x90

문제사항 제시

오버부킹 이슈를 해결하려고 PESSIMISTIC_WRITE 락을 분명 걸었는데 락 적용이 되지 않았다. 

 

코드로 예시를 들어보겠다.

 

Customer 엔티티 코드이다.

@Entity
@Table(name = "customer")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class CustomerEntity extends BaseEntity {

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

    private String name;
    private String email;
    private String password;
    private String tel;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ScheduleEntity> schedules = new ArrayList<>();

    ...

 

Schedule 엔티티 코드이다.

@Entity(name = "schedule")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString(exclude = {"studySeat", "customer"})
@Where(clause = "state = 'RESERVED' && started_time >= NOW()")
public class ScheduleEntity {

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

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
    @JoinColumn(name = "customer_id", referencedColumnName = "id")
    private CustomerEntity customer;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
    @JoinColumn(name = "study_seat_id", referencedColumnName = "id")
    private StudySeatEntity studySeat;

    @Column(name = "started_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime startedTime;

    @Column(name = "end_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime endTime;

    @Enumerated(value = EnumType.STRING)
    private ScheduleState state;
    
    ...

 

StudySeat 엔티티 코드이다.

@Entity
@Table(name = "study_seat")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class StudySeatEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String seatNumber;
    private boolean occupied;

    @OneToMany(mappedBy = "studySeat", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ScheduleEntity> schedules = new ArrayList<>();
    
    ...

 

StudySeatService 코드이다.

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        Customer customer = findByCustomerId(command.getCustomerId());
        StudySeat studySeat = findByStudySeatId(studySeatId);

        check(studySeat.isReservable(command.getStartedTime(), command.getEndTime())
            , INVALID_SCHEDULE_RESERVATION_ALREADY_OCCUPIED)
        ;

        studySeat.reserve(
            customer, studySeat,
            command.getStartedTime(), command.getEndTime()
        );

        studySeatRepository.save(studySeat);

        return new CommandReserveSeatResponse(success, studySeatId);
    }

 

 

Customer, StudySeat은 Schedule과 1:M 관계이다. DDD 구조상 StudySeat 과 Schedule로 하나의 aggregate로 묶여져 있고 StudySeat이 Schedule을 제어하는 역할을 한다.

 

그래서 이와 같은 파일 구조를 가진다.

 

그래서 결론은 StudySeat의 Repository에 findById에 Lock을 걸었는데 적용되지 않았다.

여러개의 쓰레드를 만들어서 한꺼번에 실행 시키는 테스트 코드를 실행시켰는데, 콘솔에 찍히는 query문을 여러번 눈을 씻고 찾아봐도 select for update가 계속 찍혔는데도 내가 원했던 결과가 나오지 않았다.

 

 

해결책 모색

생각 해보니 Service 코드를 보면 Customer 부터 찾아오는 것을 확인할 수 있었다.

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        Customer customer = findByCustomerId(command.getCustomerId());
        StudySeat studySeat = findByStudySeatId(studySeatId);

이 순서대로면 CustomerRepository에 락을 적용해야 원하는 결과가 나온다.

 

왜냐하면 Customer와 Schedule도 1:M으로 매핑되어 있기 때문에 Customer먼저 찾으면 

 

이런꼴이 되기 때문에 isReservable 메소드(예약이 가능한지 검증하는 메소드)에 들어가면 schedule이 비어 있어서 모두 통과가 되는 것이다. 그러면 락이 걸리는 StudySeat 부터 검색하게 하면 해결이 된다.

 

 

적용

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        StudySeat studySeat = findByStudySeatId(studySeatId);
        Customer customer = findByCustomerId(command.getCustomerId());

 

결과

Customer를 먼저 찾아오게 되면 여러개 쓰레드 모두가 매핑된 Schedule이 비어있다고 인식된다. 그래서 락이 걸리는 StudySeat 객체 부터 찾아오게 하면서 이 문제를 해결했다. 락을 걸 때 순서 또한 중요하다고 느꼈다.

728x90

문제사항 제시

나는 테스트 환경에서 h2 in-memory db를 사용하고 있었다. 통합 테스트를 하려고 했는데 에러가 몇시간 동안 발목을 잡았다.

 

ddl-auto: validate를 해놓은 상태이고 엔티티와 디비가 맞는지만 확인했다.

 

그런데 Flyway가 실제 디비에 어떻게 적용시켰는지를 알아야 엔티티를 디비에 맞추든, 디비를 엔티티에 맞추든 할 수 있었다.

(사실 엔티티를 디비에 맞추면 OCP 위반이기 때문에 엔티티를 디비에 맞추어야했다.)

 

그렇게 하려면 디비의 상황을 알아야 했는데 아무리 h2.console.enabled=true 로 해놓아도 http://localhost:8080/h2 가 열리지 않았다.

 

 

해결책 모색

애초에 나는 application-test.yml, application.yml을 나눠놓았다.

 

그래서 application.yml 에서

 

spring:
  profiles:
    active: test

 

를 하고 애플리케이션을 실행시켜서 h2 console에 들어갈 수 있었다.

 

그 결과, 실제 어떻게 Flyway가 디비에 적용시켰는지, 왜 jpa가 매핑을 못시키고 있었는지 원인을 알 수 있었다.

 

 

원인

테이블들이 기본적으로 uppercase로 적용 되어 있었다.

 

적용

원인을 해결하기 위해 

 

datasource:
  url: jdbc:h2:mem:skka-for-test;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE

 

결과

 

맨 뒤에

;DATABASE_TO_LOWER=TRUE

을 추가 해주니까 됐다.

728x90

오버부킹(Overbooking) 이란

오버부킹 이슈는 여러명의 사용자가 동시에 같은 자원을 예약하려고할 때 발생한다.

문제상황 제시

현재 SKKA프로젝트 도메인에서 오버부킹 이슈가 발생할 수 있는 가능성이 많다고 생각된다.
대표적으로 특정해 몇월 몇일에 스케줄을 예약하려고 하는데, 마침 그 날이 수요가 많은 날일 때 예약하려고 하는 요청들이 몰릴 것이다.
그렇게 된다면 처음에 해당 날짜에 예약을 요청한 사용자가 예약에 성공 해야 공정하고 투명한 시스템이 될 것 같다.

 


< 예시 >

[현재 테이블 상황]

id customer_id study_seat_id started_time              end_time                   
1 1 1 2023-12-01 11:12:00 2023-12-01 13:12:00
2 2 2 2023-12-01 14:12:00 2023-12-01 16:12:00
3 3 3 2023-12-01 18:12:00 2023-12-01 20:12:00

 

[1번 요청]

POST /seats/1 {
  "customerId" : 1,
  "startedTime" : "2023-12-01T09:00:00",
  "endTime" : "2023-01-10T11:00:00"
}

 

[2번 요청]

POST /seats/1 {
  "customerId" : 2,
  "startedTime" : "2023-12-01T09:00:00",
  "endTime" : "2023-01-10T11:00:00"
}

 

[3번 요청]

POST /seats/1 {
  "customerId" : 3,
  "startedTime" : "2023-12-01T09:00:00",
  "endTime" : "2023-01-10T11:00:00"
}

 

1, 2, 3번 요청이 한꺼번에 요청이 쏟아졌을 때 제일 먼저 온 요청만 처리 해야하고 나머지는 이미 차지 되었다는 에러를 던져야한다.


해결방안 모색

  • lock
    • ORM (jpa)
    • ORM 레이어에서 락을 사용하면 더 높은 추상화 수준을 제공해서 락 로직을 데이터베이스 대신 애플리케이션 코드에서 처리할 수 있다.
      그러면 개발 프로세스가 간소화될 것 같고, 특히 동시 트랜잭션이 많은 애플리케이션에서 성능이 향상될 수 있을 것 같다.
      그러나 덜 견고하거나 확장 가능하지 않을 수 있으며 일관성을 유지하고 교착 상태를 피하기 위해 추가적인 관리가 필요할 수 있을 것 같다.
    • DB
    • 데이터베이스(DB) 레이어의 락은 데이터베이스 수준에서 구현되므로 애플리케이션 코드와 독립적이므로 더 신뢰성이 높고 견고하다.
      여기서 신뢰성이 높고 견고하다는 의미는 네트워크 장애, 하드웨어 오류, 잘못된 애플리케이션 사용 등 예기치 못한 상황에서도 데이터베이스는 올바르게 작동 하고 오류 없이 기능한다는 것을 보장하는 것을 말한다.
      그리고 복잡한 데이터 관계나 여러 애플리케이션이 동일한 데이터베이스에 액세스하는 경우 특히 효과적이다. 그러나
      구현과 관리가 더 복잡할 수 있고, 성능에 더 큰 영향을 미칠 수 있다.
  • ORM 레이어의 락과 데이터베이스 레이어의 락 중 어떤 것이 나은지는 애플리케이션의 특정 요구 사항에 따라 다를 것 같다.

찾은 해결방안 적용

ORM lock은 애플리케이션 레밸에서 구현이 되고, 데이터의 lock을 얻고, 해제 하는 행위를 ORM에 의존을 해야한다.
그리고 ORM lock은 데이터베이스 lock과 유사한 기능을 제공하지만, 데이터를 먼저 데이터베이스에서 검색한 다음 잠금을 획득해야 하기 때문에 성능이나 신뢰성이 부족하다.
이와 같은 추가적인 프로세스는 overhead를 추가하고 race condition을 야기하기 때문에 ORM lock은 오버부킹을 해결하는데 적합하지 않다.

결과

결국은 exclusive lock을 사용하면 된다.

그러므로

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)

을 사용해주면 되겠다.

+ Recent posts