Series/실전!

Repository 가볍게 관리하기

Hyunec 2022. 8. 1. 14:05
 

GitHub - Hyune-c/blogcode-repository-di: Repository 의존성을 가볍게 관리하기

Repository 의존성을 가볍게 관리하기. Contribute to Hyune-c/blogcode-repository-di development by creating an account on GitHub.

github.com

작고 단순한 피쳐를 개발할 때는 크게 신경 쓰지 않던 것들이 공부하고 실무를 할수록 의문이 생기곤 합니다.
그중 하나인 Repository를 개발하면서 느낀 점을 기록해봅니다.

  • 도메인 로직과 영속성 영역의 경계는 어디일까? 
  • 어떻게 하면 인적 실수를 줄이고 생산성을 높일 수 있을까?

 

지금의 회사에서 개발하는 애플리케이션은

B2B 성격이기에 그리드 형태의 조회 로직이 많습니다.
그렇기에 개발 편의성을 위해 포기한 부분이 있고, 그중 하나가 Repository의 거대함입니다.

  • 도메인 로직이 영속성 영역에 위치하고, 거대하고 복잡한 쿼리로 구현되어 있습니다.
    • 4천 줄 이상의 클래스도 존재하며 로직 이해가 힘듭니다.
    • DB의 용어를 그대로 사용해 가독성이 떨어집니다.

 

v1 - 기존의 실무에서 사용되던 방법

@Repository
public interface V1StudentRepository extends
		JpaRepository<Student, Long>,
		V1StudentQuerydslRepository,
		V1StudentJooqRepository {

	// 이름으로 조회하는 경우 JpaRepository를 활용합니다.
	Student getByName(final String name);
}

interface V1StudentJooqRepository {

	// 이름과 나이로 조회하는 경우 Jooq를 활용합니다.
	Student getByNameAndAge(String name, Integer age);
}

interface V1StudentQuerydslRepository {

	// 나이와 주소로 조회하는 경우 Querydsl을 활용합니다.
	Student getByAgeAndAdress(Integer age, String adress);
}
  • 쉬운 기능은 JpaRepository를 통해 생성되는 메서드를 활용합니다.
  • 일부 복잡한 쿼리는 Jooq or Querydsl을 활용합니다.

 

v2 - 복잡한 조회만 하고 싶습니다

복잡한 조회 쿼리만 필요한 경우에는 Jooq와 Querydsl만 활용하고 싶습니다.
JpaRepository를 활용하면 사용되지 않는 CRUD 메서드가 다수 생성되기 때문입니다.

@Repository
public interface V2StudentRepository extends
		V2StudentQuerydslRepository,
		V2StudentJooqRepository {

}

interface V2StudentJooqRepository { ... }
interface V2StudentQuerydslRepository { ... }
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.blogcoderepositorydi.v2.repository.V2StudentRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

JpaRepository를 빼면 되겠지라고 단순하게 접근했지만, v2는 작동하지 않습니다.
V2StudentRepository는 interface이기 때문에 DI를 할 수 있는 구현체가 존재하지 않기 때문입니다.

 

 

Spring Data JPA 는 어떻게 interface 만으로도 동작할까? (feat. reflection, proxy)

Spring Data JPA를 공부하면서 궁금한 것이 있었습니다. public interface MemberRepository extends JpaRepository { List findAllByName(String name); } 위와 같이 MemberRepository는 인터페이스고, @Reposito..

pingpongdev.tistory.com

 

v3 - v2를 보완하기 위해 기본 구현체를 만듭니다

public interface V3StudentRepository {

 Student getByNameAndAge(String name, Integer age);
 Student getByAgeAndAdress(Integer age, String adress);
}

@RequiredArgsConstructor
@Repository
public class V3StudentRepositoryImpl implements V3StudentRepository {

	private final V3StudentJooqRepository v3StudentJooqRepository;
	private final V3StudentQuerydslRepository v3StudentQuerydslRepository;

	@Override
	public Student getByNameAndAge(final String name, final Integer age) {
		return v3StudentJooqRepository.getByNameAndAge(name, age);
	}

	@Override
	public Student getByAgeAndAdress(final Integer age, final String adress) {
		return v3StudentQuerydslRepository.getByAgeAndAdress(age, adress);
	}
}

interface V3StudentJooqRepository { ... }
interface V3StudentQuerydslRepository { ... }
  • interface mixin 대신 delegate 패턴을 활용합니다.
  • 기능이 변경되어도 사용자 코드에 영향이 없는 느슨한 결합을 만족하지만, 코드 양이 늘어난 것이 조금 불편합니다.

 

v4 - Spring Data Repository로 기본 구현체를 만듭니다

public interface V4StudentRepository extends
		// org.springframework.data.repository.Repository
		Repository<Student, Long>, 
		V4StudentQuerydslRepository,
		V4StudentJooqRepository {

}

interface V4StudentJooqRepository { ... }
interface V4StudentQuerydslRepository { ... }
  • v3에 비해 코드 양이 줄어듭니다.
  • 기능이 늘어나도 mixin Repository에만 수정이 일어납니다.
  • 하지만 Repository<>는 스프링 프레임워크에 의존성을 가집니다.
혹자는 도메인 로직에서 프레임워크 의존성을 완전히 배제해야 된다고 말합니다.
하지만 변경의 가능성이 매우 낮고, 개발 편의성이 명백히 향상된다면 팀의 예외로 정할 수 있다고 생각합니다.
스프링 없는 백엔드 개발이라니.. 끔찍하군요.

 

v5는 조금 더 학습 후 별도의 글을 작성 예정입니다. 지금의 내용은 참고만 해주세요.

v5 - 인터페이스와 구현체를 패키지로 분리한 구조

// v5.domain
public interface V5StudentRepository {

	Student getByNameAndAge(String name, Integer age);
	Student getByAgeAndAdress(Integer age, String adress);
}


// v5.infrastructure
public interface V5StudentJpaDao extends
		V5StudentRepository,
		Repository<Student, Long>,
		V5StudentJooqDao,
		V5StudentQuerydslDao {

}

interface V5StudentJooqDao { ... }
interface V5StudentQuerydslDao { ... }
  • Repository는 객체의 상태를 관리하는 저장소로서 domain 레이어에 위치합니다.
  • Dao는 영속성 상태를 관리하는 구현체로서 infrastructure 레이어에 위치합니다. 

DDD의 아이디어를 기반으로, 레이어를 엄격하게 구분해서 유연하고 모듈화에 강합니다.
하지만 러닝 커브가 높고 개발자마다 해석이 조금씩 다르기에 팀에 실제로 적용하기까지는 꽤 어려움이 있습니다.

 

Repository와 Dao의 차이점.

Repository와 Dao의 차이점에 대한 논쟁은 이전부터 끝없이 진행되어 왔다. 이번 포스팅에서는 Repository와 Dao의 차이에 대한 나의 생각을 논해보록한다. 필자가 생각하기에 Dao와 Repository의 차이점을

bperhaps.tistory.com

 

DAO vs Repository Patterns | Baeldung

Understand the difference between the DAO and Repository patterns with a Java example.

www.baeldung.com

 


 

여기까지가 고민을 해결하기 위해 공부한 이론입니다.
하지만 실무에서의 개선은 조금 다릅니다.

 

실무에서의 개선

public interface BizSlipRepositoryJooq {

	/**
	 * [손익현황] 손익현황 조회
	 */
	List<ProfitAndLossStatus> findProfitAndLossStatus(ProfitAndLossStatusFilterInput input);

	... // 30개가 넘는 public 메서드가 존재함
}

@Repository
public class BizSlipRepositoryJooqImpl implements BizSlipRepositoryJooq {
	@Override
	public List<ProfitAndLossStatus> findProfitAndLossStatus(final ProfitAndLossStatusFilterInput input) { ... }
    
    ... // private 메서드를 포함하여 총 4000 line의 구현 클래스
}

BizSlipRepositoryJooq는 Jooq로 복잡한 조회 쿼리를 처리하기 위해 만들어진 interface입니다.
하지만 30개가 넘는 public 메서드를 구현하기 위해 4000 line의 구현 클래스가 존재합니다.
로직을 이해하는 것부터 개선과 기능 수정을 시도하는 것조차도 너무 힘든 일입니다.

 

// before
public interface SlipRepository extends JpaRepository<BizSlip, Long>, BizSlipRepositoryCustom, BizSlipRepositoryJooq {
	...	
}


// after
public interface BizSlipRepository extends
		JpaRepository<BizSlip, Long>,
		BizSlipRepositoryCustom,
		BizSlipFindProfitAndLossStatusRepositoryJooq,
		BizSlipRepositoryJooq {
	...
}

public interface BizSlipFindProfitAndLossStatusRepositoryJooq {

	/**
	 * [손익현황] 손익현황 조회
	 */
	List<ProfitAndLossStatus> findProfitAndLossStatus(ProfitAndLossStatusFilterInput input);
}

// 대상 메서드만 단일 interface로 분리
@Repository
public class BizSlipFindProfitAndLossStatusRepositoryJooqImpl implements BizSlipFindProfitAndLossStatusRepositoryJooq {

	@Override
	public List<ProfitAndLossStatus> findProfitAndLossStatus(final ProfitAndLossStatusFilterInput input) { ... }
}

대상 메서드만 단일 interface로 분리했습니다.

  • 대상 로직이 하나의 클래스로 응집되었습니다.
  • 4000 line의 거대 Impl에서 300 line을 분리해 가독성을 높였습니다.
  • 로직의 수정 없이 단순히 클래스만을 분리했기에 가볍게 접근할 수 있습니다.

하지만 이 방식은 부족한 부분도 존재합니다.

  • 기능의 개수만큼 클래스가 생기고 네이밍의 어려움이 생깁니다.
  • 접근 제한자 활용이 힘듭니다.

 

마치며

장황하게 설명한 이론과는 다르게 실무는 협업이기에 충분한 의도와 근거를 가지고 점진적인 개선을 해야 됩니다.
그래서 실무에서는 가독성 향상을 위한 Repository 분리만을 선택했고 가볍게 접근할 수 있었습니다.
실무에서의 개선은 이렇게 접근해야 된다고 생각합니다.

마찬가지로 DDD가 좋다고 하여 v5의 구조를 바로 적용해서는 안됩니다.
중요한 것은 레거시와 생산성, 러닝 커브를 고려한 지속적인 개선을 시도할 수 있는 사이클을 만드는 것 입니다.
이 관점에서 지금의 회사에는 v4의 구조가 적절하다고 생각하기에 이 방향으로 개선해 나갈 생각입니다.