처음 작성해보는 것이며, 문제 해결 과정이 지난 후에 작성하는 것이라 간단하게 작성하였다.
(추후에 각 트러블 슈팅을 하나씩 기록으로 더 자세히 옮기면 좋을 것 같다.)
또한, 오류 형식의 문제는 아니지만 고민 지점을 해결해 나가는 과정도 함께 작성하였다. (간단하지만 수많았던 오류들을 모두 기록하지 못하였기 때문이다.)
1. 문제 (또는 고민지점)
2. 원인 (이유)
3. 해결 과정 (방향을 찾아가는 과정)
4. 해결 방법 (선택)
도메인끼리의 강결합과 애그리거트 분리 과정
1. 고민지점 콘서트 예약 서비스의 콘서트 정보 조회의 핵심 기능인 '예약 가능한 좌석 조회' api 에서 예약 도메인을 바라봐야 하고, 예약 도메인이 콘서트, 사용자 도메인을 모두 가지고 있는 것에 도메인들이 기능과 역할에 대한 분리가 잘 되지 않았음을 느낄 수 있었다.
2. 원인 기존 테이블 설계와 도메인 모델을 작성할 때 객체 자체를 연관관계로 넣어 결합을 강하게 가져갔다. 도메인 애그리거트에 대한 개념과, 내가 구현하는 서비스의 행위에 대해 우선순위가 잘 잡혀있지 않았기 때문이다.
3. 해결 과정 도메인 애그리거트와 루트 애그리거트에 대하여 학습하고, 나의 콘서트 서비스 도메인을 분리해보았다.
4. 선택한 방법 도메인 애그리거트를 나누고, 서로 행위에 대하여 영향을 미치지 않는 관계에서는 객체 연관관계를 가져가기 보다는 pk를 들고 한번 더 조회하는 것을 택하여 설계해 보았다. 물론 각 장단점이 있지만, 이번 과제 과정에서는 도메인 별 분리를 더 신경써서 구현해보려고 하였다.
동시성 제어 처리 중 에러
1. 문제 동시성 제어 처리 테스트 중 낙관적 락 구현, 비관적 락 구현 모두 에러 비관적 락으로 동시성 제어 테스트 중 에러
2. 원인
간단하게 말하면 낙관적 락, 비관적 락 모두 데이터의 변경을 제어하는 것이다. 나는 insert 문을 동시성 제어하려고 했으므로, 동시성 문제를 해결하지 못함.
MySQL에서 사용되는 락(LocK)은 크게MySQL 엔진 레벨의 락과스토리지 엔진 레벨의 락으로 나눠볼 수 있다.
스토리지 엔진 레벨의 락
스토리지 엔진(InnoDB)에서 제공하는 락(Lock, 잠금)이 있다.
기본적으로 비관적 락(Pessimistic locking)을 사용한다.
비관적 락: 트랜잭션에서 변경하려는 레코드에 대해 락을 획득하고 쿼리를 수행하는 방식 (같은 레코드를 변경하려는 경우가 많을 것이라고 비관적인 생각을 하기 때문에 비관적 락)
낙관적 락: 트랜잭션에서 락 없이 일단 쿼리 수행을 하고 마칠 때 서로 다른 트랜잭션에서 충돌이 있었는지 확인하고 문제가 있으면 충돌이 난 트랜잭션을 롤백하는 방식 (같은 레코드를 변경하려는 경우가 거의 없을 것이라고 낙관적인 생각을 하기 때문에 낙관적 락)
보통 대규모 트래픽을 처리하는 애플리케이션에서는 성능 이슈 때문에 락을 최소화 해야하기 때문에 낙관적 락으로 변경하여 사용하기도 한다.
스토리지 엔진(InnoDB)이 제공하는 락
✔️ 참고로 아래에서 "인덱스 레코드" 라는 표현을 사용할 것인데, 그냥 "인덱스"로 이해하면 된다. 필자는 잠깐 헷갈렸기에 비슷하게 느끼는 사람이 있을까 봐 참고로 적어놓았다. (인덱스도 레코드 형식으로 저장되니까...?)
레코드 락(Record lock)
SELECT c1 FROM t WHERE c1=10 FOR UPDATE;일 때, c1=10인 인덱스 레코드에 락을 걸어서 수정, 삽입, 삭제등의 다른 트랜잭션을 막는다. (이때 c1은 유니크하다는 가정 한다.)
실제 테이블의 레코드에 대해 락을 걸지 않고 인덱스 레코드에 락을 건다.
따로 생성한 인덱스가 없는 테이블은 InnoDB가 자체적으로 생성한 클러스터 인덱스를 이용해 락을 건다.
기본키나 유니크 키에 의한 변경 작업은 갭 락 없이 딱 인덱스 레코드에만 락을 건다.
갭 락(Gap lock)
인덱스 레코드와 인접한 앞/뒤 사이 공간에 락을 거는 것인데, 개념적인 용어로 단독으로는 사용되지 않고 넥스트 키 락에서 사용된다. (자세한 건 넥스트 키 락에서 예제를 보자)
갭 락은 READ_COMMITED 이하에서는 거의 발생하지 않고 REPEATABLE_READ 이상 격리 수준일 때에 주로 발생한다. ('거의'라고 한 이유는 외래 키 검사나 중복 키 검사할 때는 READ_COMMITED에서도 발생하기 때문이다.)
INSERT INTO ... ON DUPLICATE KEY UPDATE는 INSERT를 하려는데 "유니크 키"나 "기본 키"에 중복이 일어나면 UPDATE로 동작하도록 한다. 당연히 중복된 키가 없으면 INSERT로 동작한다.
넥스트 키 락(Next key lock)
레코드 락과 갭 락을 합쳐놓은 형태다. 인덱스 레코드도 잠그고 그 인덱스 레코드 앞, 뒤 갭도 잠근다.
SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE;일 때, c1=15 인 레코드를 insert 하는 트랜잭션이 있다면 막는다. 반드시 인덱스 레코드 사이에 있는 갭만 락을 거는 게 아니라 제일 앞 또는 뒤에 있는 인덱스 레코드의 갭도 락을 건다. 예를 들어 c1=10인 레코드 인덱스 바로 앞에 c1=8인 레코드 인덱스가 있는 상태라면, c1=9인 레코드를 insert 하려고 하면 막힌다. (그 갭에도 락이 걸려있기 때문에!)
오토 인크리먼트 락(Auto increment lock)
MySQL에서 자동 증가하는 숫자 값을 채번하기 위해 AUTO_INCREMENT라는 컬럼 속성을 정의할 때가 있다. 이때 같이 INSERT 하려는 요청이 올 때 거는 락이다.
4. 해결 방법 일단, 가장 간단한 제어 방법으로 레코드 락을 선택하였다. concertDateId와 seatId에 유니크 인덱스 조건을 걸어 레코드 자체 중복이 불가하고, 이에 대한 예외가 발생했을 경우 커스텀 예외 처리를 해 주었다.
또 다른 방법으로는 낙관적 락이나 비관적 락을 사용할 수 있도록 예약 요청을 할 때 insert 될 때마다 예약 수를 +1씩 update 처리를 추가해주면 락이 잘 동작할 것이다. 이 방식은 다음에 적용해보면 좋을 것 같다.
List.of() 객체 리스트 정렬 시 변경 불가능
1. 문제 concertDateList.sort(Comparator.comparing(ConcertDate::getConcertDate));
java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142) at java.base/java.util.ImmutableCollections$AbstractImmutableList.sort(ImmutableCollections.java:261)
2. 원인 간단했다. 변경 불가능한 리스트를 변경하려고 해서..
3. 해결 과정 변경 불가능한 컬렉션 종류 : Java에서는 List.of(), Collections.unmodifiableList(), Map.of() 등의 메소드로 생성된 컬렉션들이 변경 불가능한 상태이다.
4. 해결 방법 생성자로 ArrayList 만들어서 사용하기 이런 간단한 거 기본으로 좀 알기!!! 를 위해 적어봤다.
트랜잭션을 분리하기 위해 @EventListener 사용했으나 트랜잭션 분리되지 않음
1. 문제 트랜잭션 분리되지 않음
2. 원인 @EventListener는 트랜잭션의 상태와 무관하게 이벤트를 처리
3. 해결 과정 @TransactionEventListener는 트랜잭션의 상태에 따라 이벤트를 처리할 수 있음. 이벤트 처리를 트랜잭션의 특정 단계(예: 커밋, 롤백)에 연결할 수 있는데, @TransactionEventListener는 phase 속성을 통해 트랜잭션의 다양한 단계 중 언제 이벤트를 처리할지 지정할 수 있다. 사용할 수 있는 TransactionPhase 옵션에는 BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION 등이 있다.
4. 해결 방법 @TransactionEventListener 사용하여 트랜잭션 커밋이 끝난 이후에 처리되도록 하였다.