수강 신청 미션 - layered architecture with clean architecture

 

GitHub - pbg0205/lecture2024

Contribute to pbg0205/lecture2024 development by creating an account on GitHub.

github.com

 

[1] 레이어드 아키텍처 with 클린 아키텍처

(1) 클래스 의존성

레이어드 아키텍처 with 클린 아키텍처 - 클래스 의존성

기존 레이어드 아키텍처는 단방향 참조 구조로 인해 데이터베이스(DB) 계층의 변경이 서비스 계층에 직접적인 영향을 미치며, 이는 개방-폐쇄 원칙(OCP)을 만족하지 못하는 한계가 있었다.

 

 이를 개선하기 위해 클린 아키텍처의 기본 개념을 도입하여 비즈니스 로직으로 데이터 계층과 API 계층이 의존하도록 구조를 재설계했다. 기존의 Service → Repository 의존성을 역전시키기 위해 의존성 역전 원칙(DIP)을 적용하여 추상화 계층을 도입하였고, 이로써 행위(interface)를 기반으로 코드를 작성하여 세부 구현의 변경에도 유연하게 대응할 수 있는 구조를 구축할 수 있었다.

 

 

(2) 패키지 구성에 관한 고민

패키지 의존성

클래스 의존성 관계가 지켜지기 위해서는 패키지 의존성 또한 만족해야 한다고 생각했다. 패키지 의존성 또한 business 로 의존하도록 구성하였고, business 는 엔티티의 상태 관리를 위해 domain 패키지를 의존하도록 구성했다.

 

(3) DSM 을 활용한 의존성 모니터링

business -> domain, common 만 의존한다 (common 은 custom annotaion 때문임,,)

패키지, 클래스 의존성을 관리하기 위해 IntelliJ 에서 제공하는 DSM(Dependency Structure Matrix) 를 활용했다. 패키지 간의 의존성을 확인할 수 있을 뿐만 아니라 특정 클래스의 의존성까지 확인할 수 있다. DSM 를 통한 모니터링이 직관적인 의존 관계 확인과  핸들링하기 좋은 방법인 것 같다.

  • intellij DSM 사용 방법 : Code > Analyze Code > Dependency Matrix 클릭

 

 

[2] DB 설계를 위한 기준 정립

(1) FK 를 사용하지 않는 이유

  • FK 설정은 락 전파로 인한 성능, 동시성 문제를 야기할 수 있다. 또한, 조인 성능에 관한 이슈는 별도의 인덱스를 선언해 해결 가능하다고 판단했다.

[FK 장점]

1. 데이터 무결성 보장과 일관성 유지, 자동 데이터 갱신 관리(e.g. CASCADE) 를 보장한다.
2. FK 를 참조하는 테이블은 index 가 생성되어 join 연산을 실행할 때, 조인 성능을 향상시킨다.

[FK 단점]

  1. 잠금 전파 : FK 제약 조건 설정된 경우, 부모-자식 테이블 간의 데이터 일관성을 위해 테이블에 잠금을 확장한다. DML 및 DDL 작업이 동시에 실행되는 것을 방지하기 위함이다.
  2. 데드락 발생 가능성 : FK 는 부모 테이블이나 자식 테이블에 데이터 존재를 확인하는 작업이 필요해 잠금이 여러 테이블로 전파되어 데드락이 발생활 확률 높다.

 

(2) timestamp 사용 시, 고려사항

  1. timestamp 는 타임존(time zone)을 고려한 타입이다. 입력된 시간을 UTC 로 변환 저장하고, DB 서버의 timezone 에 맞게 변환된다. 정확한 현지 시간을 반환하기 때문에 수강 신청 도메인의 경우에는 다른 지역에서 동일한 시간 정보 조회가 필요하다고 생각했다.
  2. 2038년 문제(Y2K38) : 유닉스 시간에서 32비트 정수를 사용하는 시스템2038년 1월 19일 03:14:07 UTC 이후 초 값이 오버플로우되어 음수로 처리됩니다. 이로 인해 시간이 1901년 12월 13일 20:45:52로 잘못 표시되거나, 초기값(1970년 1월 1일 00:00:00)으로 돌아가는 오류가 발생한다.
  3. 이를 대비하기 위해 64비트 시스템(e.g. x86-64, arm64) 을 사용하는지 확인하거나 timestamp 대신에 BIGINT 를 사용을 고려해본다.

 

[3] MVCC with Lock

(1) MVCC(Multi Version Concurrency Control) ??

MVCC 는 동시 접근을 허용하는 DB 에서 동시성을 제어하는 기법 중 하나로, snapshot 을 기반한 데이터의 여러 버전 관리를 통해 잠금없는 읽기를 제공한다. 이를 통해 높은 동시성과 일관성 보장, 잠금 최소화를 하여 성능을 상향시킨다.

 

 

1. MySQL MVCC의 특징

  1. 스냅샷 읽기(Snapshot Read)
    • MVCC는 각 트랜잭션이 데이터를 읽을 때, 그 트랜잭션이 시작된 시점의 데이터 스냅샷(특정 시점의 데이터 상태)을 제공한다.
    • 다른 트랜잭션이 데이터를 변경해도 현재 트랜잭션은 자신의 스냅샷을 사용하여 일관된 읽기를 유지한다.
  2. 락 없이 동시성 제공
    • 읽기 작업은 데이터를 잠그지 않고, 쓰기 작업과 동시에 실행될 수 있다.
    • SELECT는 읽기 잠금을 피하고 데이터 버전 관리를 통해 동시성을 극대화 할 수 있다.
  3. Undo Log를 통한 버전 관리:
    • MVCC는 데이터의 이전 버전을 Undo Log에 저장하여, 트랜잭션이 필요할 때 과거 데이터를 복원할 수 있도록 한다.
    • 트랜잭션이 데이터를 읽을 때, 트랜잭션 타임스탬프를 기반으로 undo log 를 참조하여 과거 데이터를 복원한다.
    • 필요없는 undo log 는 purge thread 에 의해 삭제된다.

 

(2) Transaction isolation level

1. Transaction isolation level??

트랜잭션(Transaction) 은 DB 상태 변경을 위해 수행하는 논리적인 작업 단위를 말한다. 트랜잭션을 사용하는 이유 중 하나는 데이터 정합성 보장이다. 이를 트랜잭션 격리 레벨을 통해서 제어한다. 트랜잭션 격리 레벨은 여러 트랜잭션을 동시에 처리할 때, 선행 트랜잭션이 변경 혹은 조회하고 있는 데이터를 후행 트랜잭션이 조회 허용여부를 결정하여 데이터 정합성 보장을 지원하는 기능이다.

 

 

REPEATABLE READ 동작 원리 그림

  1. READ COMMITED : 레코드 조회는 커밋된 데이터인 경우에만 조회가 가능하도록 지원한다. 커밋되지 않은 경우 undo log 에서 데이터를 읽는다. 이 레벨은 Dirty Read 문제를 방지할 수 있다.
  2. REPEATABLE READ : 트랜잭션은 Tx-id < undo log 번호만 읽는 것을 보장한다. 후행 트랜잭션의 중간에 update 처리된 레코드를 선행 트랜잭션은 undo log 에서 데이터를 읽어 UNREPEATABLE READ 문제를 방지한다.
  3. SERIALIZABLE : 가장 높은 격리 단계이며, 선행 트랜잭션이 공유 장금을 획득해 후행 트랜잭션이 절대 접근할 수 없는 설정이다. 이 레벨은 PHANTOM READ 문제를 방지할 수 있다. (PHANTOM READ : 레코드 사이에 도중에 데이터가 삽입되는 경우)

 

2. FOR UPDATE 처리할 때 후행 쿼리는 읽을 수 있을까??

-- 예시 1
SELECT * FROM USER WHERE id = 1;
SELECT * FROM USER WHERE id = 1; -- READ ❌

-- 예시 2
SELECT * FROM USER WHERE id = 1;
SELECT * FROM USER WHERE id = 1; -- READ ⭕️
  1. 예시1 : FOR UPDATE 는 레코드에 락을 걸어 디스크의 레코드를 읽도록 동작한다. 그러므로 후행 쿼리는 잠금으로 인해 접근할 수 없다.
  2. 예시2 : FOR UPDATE 를 통해 레코드에 락을 걸어 디스크의 레코드를 읽는다. 하지만 단순 SELECT 문은 undo log 를 읽어 READ 가 가능하다.

 

[4] KPT 회고

  • Keep
    • 단위 테스트를 기반으로 TFD 를 계속해서 연습하자.
    • 로직을 적용한 이유를 충분히 설명할 수 있도록 노력하자. (e.g. FK 미선언 이유, timestamp, 제약 사항..)
  • Problem
    • 테스트 케이스 네이밍이 부족하다. 구체적인 테스트 케이스 작성과 네이밍에 관해 신경써서 작성하자.
      • (given - when- then 기반)
    • 네이밍 스킬이 부족하다. 네이밍이 길어도 한 눈에 파악할 수 있는 네이밍 작성을 연습하자.
  • Try
    • 문서화 수준이 만족스럽지 못하다. best practice 를 통해 개선해보자.
    • DirtyChecking 은 모든 필드를 업데이트해 네트워크 부하를 증가시키므로 @DynamicUpdate 적용을 검토해보자.
    • 객체 생성 시, 요구사항에 맞는 validation code 를 꼼꼼하게 작성하자.
    • 동시성 문제를 검증하기 위해 테스트에 Repository <- ScheduleCoreRepository (or FakeScheduleCoreRepository) 로 구분하여 사용하는 방법 좋은 것 같다.

 

Reference