Ordinary
About

@Repository

profileordilov / 2022. 4. 17

이전에 알고 있던 부분

Spring을 사용하면서 데이터에 접근하는 클래스명의 접미사로 repository를 사용해왔습니다. 그리고 JpaRepository를 상속받은 인터페이스는 @Repository 어노테이션 없이도 동작하는 것까지 알고 있었습니다. 그런데 왜 repository라는 이름을 쓰고 어노테이션이 어떤 역할을 해주는지 알지 못하고 있어서 정리해보려고 합니다.

Repository?

Repository는 소프트웨어에서 흔히 저장소를 의미합니다. 대표적으로 Git을 원격으로 저장하는 저장소로 Git Repository, java 의존성 패키지들을 저장하는 Maven Repository 등이 있습니다. 여기까지 생각해보면 Repository를 단순히 저장소라고도 생각할 수 있습니다.

하지만 @Repository 에 설명을 보면 추가적인 개념이 더 들어있습니다.

Indicates that an annotated class is a "Repository", originally defined by Domain-Driven Design (Evans, 2003) as "a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects".

Repository는 흔히 DDD라고 불리는 도메인 주도 개발 책에서 비롯된 개념인 것을 알 수 있습니다. 해석해보면 객체 집합에서도 동작하는 저장, 조회, 조회 동작을 캡슐화하는 기법을 의미합니다. 추가적인 설명으로 Data Access Object라는 개념하고도 맞물려있다고 생각할 수 있습니다.

@Component

개념적인 것 외에도 @Repository 빈을 검색하는데에도 쓰입니다. @Repository는 @Component를 포함하고 있는데 빈으로 사용할 클래스를 검색하는데 사용됩니다. 더 들어가보면 @Component는 @Indexed 어노테이션을 포함하고 있습니다.

@Indexed

@Indexed 어노테이션은 말 뜻 그대로 관련된 인덱스를 생성하게 됩니다. 예를 들어 @Component 어노테이션을 다른 어노테이션이 포함하고, 그 어노테이션을 클래스에 사용하는 경우 두 가지가 인덱싱 됩니다.

  • @Component 를 기준으로 찾을 때 해당 클래스가 검색됩니다.
  • @Component 를 포함한 어노테이션을 기준으로 찾을 때 해당 클래스가 검색됩니다.

이 점을 생각해보면 @Repository를 사용한 클래스를 기준으로 검색이 가능해집니다.

@Repository 예외 처리

이외에도 @Repository는 예외를 추가적으로 처리해줍니다.

A class thus annotated is eligible for Spring DataAccessException translation when used in conjunction with a PersistenceExceptionTranslationPostProcessor.

설명을 보면 PersistenceTranslationPostProcessor를 통해 DataAccessException로 변환해줍니다.

DataAccessException

This exception hierarchy aims to let user code find and handle the kind of error encountered without knowing the details of the particular data access API in use (e.g. JDBC). Thus it is possible to react to an optimistic locking failure without knowing that JDBC is being used.

하나씩 살펴보면 JDBC 같은 데이터를 접근하는 API에 대해 세부사항을 몰라도 에러를 처리할 수 있게 해줍니다. 그 결과로 JDBC가 그걸 사용했는지 몰라도 [낙관적 락]이 실패했을 때 대응하게 해줍니다.

As this class is a runtime exception, there is no need for user code to catch it or subclasses if any error is to be considered fatal (the usual case).

또한 SQLException 처럼 try catch로 잡거나 메서드에서 throw를 던질 필요 없이 runtime exception으로 처리됩니다. 데이터베이스에서 서버가 다운되거나 제약조건 위반같은 예외의 경우 코드 수준에서 예외를 잡아도 복구하기 힘듭니다. 따라서 DataAccessException 처럼 추상화된 runtime exception으로 전환하는 것이 좋습니다.

또 다른 문제점으로는 스프링에서는 기본적으로 Checked Exception에 대해서 실패시 롤백처리를 하지 않습니다.

@Transactional(rollbackFor = {SQLException.class}) public void save(User user) { userRepository.save(user); }

Checked Exception에 대해 롤백처리를 하려면 트랜잭션에 명시적으로 어떤 예외가 발생했을 시 롤백할지 명시해줘야 합니다. 반면에 runtime exception의 경우 스프링에서는 자동으로 롤백처리를 합니다. DataAccessException으로 변경하는 이유를 정리해보겠습니다.

  • JDBC나 JPA 같은 데이터에 접근하는 기술 세부사항을 몰라도 추상적인 레벨에서 예외를 처리할 수 있습니다.
  • SQLException처럼 처리하기 힘든 Catched Exception을 Unchecked Exception으로 전환시켜줍니다.

PersistenceExceptionTranslationPostProcessor는 @Repository에서 발생하는 예외들을 DataAccessException으로 변환해줍니다. 클래스 이름을 보면 알겠지만 영속성 예외 처리 전환 이후에 처리하는 역할입니다. 동작은 AOP로 동작하게 되는데 이때 포인트컷으로 @Repository 어노테이션를 사용합니다.

private Class<? extends Annotation> repositoryAnnotationType = Repository.class;

프록시 반환

JpaRepository 인터페이스를 상속받은 인터페이스는 내부적으로 SimpleJpaRepository 구현체를 사용합니다. 이때 프록시 객체를 사용하는데 프록시 동작을 JdkDynamicAopProxy에서 담당합니다. 간단하게 설명하면 이 때 우리가 추가적으로 지정한 쿼리 메소드들을 포함해서 구현체가 생성되어 추가되게 됩니다.

반면에 @Repository 어노테이션만 추가한 클래스의 경우는 CGLIB에서 프록시 객체를 생성합니다. 위에서 말했듯이 예외처리 변환을 하게 되는데 AOP 처리를 하게될 때 프록시 객체가 필요해서 사용하게 됩니다.

memberJpaRepository = {$Proxy96@9237} "org.springframework.data.jpa.repository.support.SimpleJpaRepository@6337afe2" memberRepository = {MemberRepository$$EnhancerBySpringCGLIB$$1@6f8f8f8f}

추가적으로 JpaRepository 인터페이스 같은 Repository 동작에 대한 인터페이스들을 보면 @NoRepositoryBean 어노테이션이 있습니다. 우리가 주로 사용하는 인터페이스는 여러 리포지토리 인터페이스들을 상속받은 인터페이스입니다. 이렇게 되면 실제 사용할 인터페이스에 대한 빈 뿐만 아니라 상위 인터페이스들에서도 스프링 빈이 생기게 됩니다. 상위 리포지토리 인터페이스에 대한 빈들은 필요하지 않으므로 @NoRepositoryBean을 명시하면 빈이 생성되지 않습니다.

정리

  • @Repository는 DDD에서 말하는 Repository 개념을 나타냅니다.
  • @Repository가 붙은 클래스들을 검색할 수 있게 해줍니다.
  • @Repository 어노테이션이 붙은 클래스는 프록시 객체를 반환합니다.
  • 해당 클래스에서 발생하는 데이터 처리 예외를 DataAccessException으로 변환해줍니다.