데이터베이스와 연동된 테스트 데이터 초기화(생명 주기 관리) 코드를 작업하며 몸소 느낀 OOP 원칙에 대해 기록합니다.

코드의 변경은 커밋에서 보실 수 있습니다.

문제 인식 상황

먼저 어떤 상황에서 OOP 원칙 중 OCP, ISP 를 따라야되는 이유를 체감했는지 요약하겠습니다.

테스트 데이터 초기화 코드를 효율적으로 관리하고자 상속을 사용하기로 했습니다.

  • 최상위 클래스에서 테스트 데이터(엔티티 형태)로 사용할 엔티티를 생성합니다.
    (영속화 대상(엔티티)에 따라 각 엔티티를 저장하는 행위를 추상 메서드로 선언합니다.)

  • 해당 클래스를 상속해, 생성한 엔티티를 EntityManger, JpaRepository를 통해 영속화 하는 클래스를 각각 선언합니다.
    • EntityManager 로 영속화 하는 경우, 영속화 주체가 entityManager 하나입니다.
    • JpaRepository 로 영속화 하는 경우, 영속화 주체가 각 엔티티마다(JpaRepository)존재합니다. (다수)
  • 이 때, JpaRepository 관련 필드, 데이터 영속화 코드를 한 곳에 모아 관리하고 싶어서
    • 데이터 생성 객체(최상위 클래스)와
    • 개별테스트 클래스(최하위 객체)를 연결하는

    중간 객체인 RepositoryDataInitializer에 현재 존재하는 모든 JpaRepository객체를 필드로 선언했습니다. 또한 상위 객체에서 선언한 추상 메서드를 구현했습니다.

  • JpaRepository를 사용하는 개별 테스트 클래스들은 RepositoryDataInitializer를 상속해서 필요한 데이터 영속화 메서드를 호출하기만 하면 개별 테스트를 진행할 수 있었습니다.

문제는 RepositoryDataInitializer에서 발생했습니다.

코드를 작성하고 나서도 이상했던 점은, 특정 테스트에서는 사용하지 않는 ...repository 인스턴스를 주입받아야 된다는 점이었습니다.

문제 명시

이후 새로운 기능 추가를 위해 테스트를 진행하고 있었습니다. 영속화 주체를 EntityManager 하나만 가지는 테스트의 경우, 아무 문제가 없었지만, 추가한 기능의 JpaRepository의 주입을 위해 RepositoryDataInitializer 필드에 선언하는 순간, 해당 객체를 상속한 모든 하위 클래스에도 추가한 기능의 JpaRepository객체 의존성이 필요했습니다.

RepositoryDataInitializer를 상속하는 모든 하위 클래스에 추가한 기능의 JpaRepository의존성을 추가하고 있자니, ‘만약 기능이 더 추가된다면? 이건 잘못됐다.’ 는 생각이 들었습니다.

불현듯 지나치는 OOP 원칙이 떠오르면서, 개별 테스트 클래스 중 일부는 실제 사용하지 않는 객체에 의존하고 있으며, 확장에 열려있지만 수정에 닫혀있지 않음을 알았습니다. 또한 영향력이 큰 객체의 수정에 대해 하위 클래스 모두가 영향을 받음을 확인할 수 있었습니다.

해결 방안

상위 클래스에서는 하위 클래스가 공통으로 의존하는 객체가 아니라면 필드로 선언하지 않는 것이 옳음을 깨달았고,

이것을 구현하기 위해 구조 변경이 필요했습니다.

EntityManager 로 영속화를 관리하는 경우, 모든 하위 클래스에서 공통적으로 사용하는 객체인 entityManager 를 상위 클래스 필드로 선언해두는 것이 효율적인 것으로 보입니다.
기능이 추가될 때면, 해당 엔티티를 영속화하는 메서드만 넣어주면 효율적으로 데이터 초기화가 가능합니다.

JpaRepository 는 엔티티 마다 영속화 객체가 따로 존재하기에, 테스트를 진행하는 개별 클래스에서 필요한 객체만 필드로 선언(의존), 데이터 영속화를 진행합니다.

방안 적용 후

따라서 아래와 같은 구조로 변경했습니다.

test_data_initializer_dependencies

  1. TestDataInitializer (데이터 생성)
    • 테스트에 필요한 데이터를 생성하는 역할을 수행합니다.
  2. EntityManagerDataInitializer (데이터 영속화)
    • 상위 객체에서 생성한 데이터를 EntityManager 를 통해 데이터베이스에 저장합니다.
    • 해당 객체를 상속해 필요한 데이터 저장 메서드를 사용합니다.
  3. RepositoryTestBase, SpringBootTestBase (컨텍스트 분리)
    • JpaRepository 를 사용해 데이터를 저장하는 경우는 두 가지의 컨텍스트로 나누어집니다.
      • 데이터베이스 관련으로 한정된 컨텍스트
      • 앱 전체 컨텍스트
  4. JpaRepository 를 사용하는 개별 테스트 클래스 (데이터 영속화 및 테스트)
    • 테스트에 사용할 repository객체를 주입하고, 상위 객체에서 생성한 데이터 중 필요한 것들만 영속화해서 테스트에 사용합니다.

결과

개별 테스트에서 의존이 꼭 필요한 repository만 사용하고, 필요한 데이터만 초기화해서 사용하게 됐습니다.
코드의 확장에는 열려있으며, 수정에는 닫혀있는 구조를 만들었습니다.

코드 작성 시, OOP 원칙을 상기하며, 이후의 수정이 기존 코드에 미칠 영향을 고려하면서 코드를 설계, 작성 하는 습관을 들여야겠습니다.