[Spring Data JPA] Spring Data JPA는 어떻게 interface만으로 동작할까?

Hocaron·2022년 7월 5일
2

Spring

목록 보기
25/44
public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByOrganization(Organization organization);

    List<Member> findByOrganizationId(Long organizationId);
}

🤔 MemberRepository는 인터페이스고, @Repository 애노테이션을 붙여 놓지도 않았는데, 다음과 같은 코드가 가능할까?


memberRepository의 실제 객체를 보니 Proxy가 주입된다.
그리고 그 Proxy는 SimpleJpaRepository를 타겟으로 가지고 있다.
결과적으로 다음과 같은 구조이다.

Repository 인터페이스를 보고가자

Repository

데이터베이스 마커 인터페이스로, 타입과 id의 타입을 설정한다. ( 마커 인터페이스 )

CrudRepository

CRUD 메서드를 제공한다. 생성, 읽기, 삭제 메서드가 제공된다.

PagingAndSortingRepository

페이징과 정렬 메서드를 제공한다.

JpaRepository

JPA 관련 메서드를 제공한다 ( CrudRepository에 없는 batch 삭제 혹은 영속성 flush 관련 기능 제공 )

SimpleRepository

CrudRepository의 기본 구현체이다. EntityManager 필드를 사용하여 실제로 데이터 영속성을 다룬다. 모든 저장소에 적용되는 공통 메서드를 추가하고 싶으면 해당 클래스를 상속하여 메서드를 구현할 수 있다.

스프링은 어떻게 동적으로 Proxy를 만들어줄까?

리플렉션

public class Item {

    public static String id = "oldId";

    private String name = "book";

    public Item() {
    }

    private Item(String name) {
        this.name = name;
    }

    private int sum(int a, int b) {
        return a + b;
    }

    @Override
    public String toString() {
        return name;
    }
}
@Slf4j
public class ItemApp {

    public static void main(String[] args) throws
        NoSuchMethodException,
        InvocationTargetException,
        InstantiationException,
        IllegalAccessException,
        NoSuchFieldException,
        ClassNotFoundException {

        Class<Item> itemClass = (Class<Item>)Class.forName("com.springstudy.jpa.item.Item");

        Constructor<Item> defaultConstructor = itemClass.getDeclaredConstructor(null);
        Item item1 = defaultConstructor.newInstance();
        log.info("item1 : {}", item1);

        Constructor<Item> constructor = itemClass.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        Item item2 = constructor.newInstance("cup");
        log.info("item2 : {}", item2);

        Field id = Item.class.getDeclaredField("id");
        log.info("id : {}", id.get(null));

        id.set(null, "newId");
        log.info("id : {}", id.get(null));

        Field name = Item.class.getDeclaredField("name");
        name.setAccessible(true);
        log.info("name : {}", name.get(item2));

        name.set(item2, "phone");
        log.info("name : {}", name.get(item2));

        Method sum = itemClass.getDeclaredMethod("sum", int.class, int.class);
        sum.setAccessible(true);
        Object result = sum.invoke(item1, 1, 2);
        log.info("result : {}", result);
    }
}

리플렉션
리플렉션 기술을 사용하면 클래스나 메서드의 메타 정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다. 심지어 private 접근 제어자가 붙어있는 메서드에도 접근할 수 있다. 이렇게 메타정보를 이용해서 클래스, 필드, 메서드 정보를 얻는다는 것은 정보를 동적으로 변경할 수도 있게 된다. 결과적으로 동적인 객체 생성, 동적 메서드 호출 기능 등을 사용 할 수 있는데 Spring에서는 DI, Proxy 등에서 리플렉션이 사용된다.
코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우
프레임워크나 IDE에서 이런 동적인 바인딩을 이용한 기능을 제공한다. intelliJ의 자동완성 기능, 스프링의 어노테이션이 리플렉션을 이용한 기능이다.

Dynamic Proxy에서 리플렉션

public interface Repository {

    void save(String itemId);
}
@Slf4j
public class SimpleRepository implements Repository {

    @Override
    public void save(String itemId) {
        log.info("Save Item. itemId = {}", itemId);
    }
}
public interface Repository {

    void save(String itemId);
}
@Slf4j
public class RepositoryHandler implements InvocationHandler {

    private final Repository target;

    public RepositoryHandler(Repository target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("save".equals(method.getName())) {
            log.info("save() in proxy");
            return method.invoke(target, args);
        }
        return method.invoke(target, args);
    }
  • Object proxy : 프록시 자신
  • Method method : 호출한 메서드
  • Object[] args : 메서드를 호출할 때 전달한 인수

Handler로 프록시를 생성

public class ReflectionTest {

    @Test
    void reflectionTest() {
        RepositoryHandler repositoryHandler = new RepositoryHandler(new SimpleRepository());
        CustomRepository customRepository = (CustomRepository) Proxy.newProxyInstance(
                CustomRepository.class.getClassLoader(),
                new Class[]{CustomRepository.class},
                repositoryHandler
        );

        customRepository.save("ITEM22");
    }
}
18:47:12.038 [main] INFO com.example.reflection.RepositoryHandler - save() in proxy
18:47:12.041 [main] INFO com.example.reflection.SimpleRepository - Save Item. itemId = ITEM22

디버깅을 내용을 살펴보면 interface는 MemberRepository, target은 SimpleJpaRepository인 것을 확인할 수 있다. 이러한 과정을 보면 스프링은 MemberRepository를 구현하는 객체를 생성해주고 있다. 처음에 나왔던 save()메소드는 target인 SimpleJapRepository에게 요청을 위임하고, 사용자가 만들었던 findAllByName() 메서드도 동적으로 만들어주고 있다.

마무리

한 줄로 정리를 해보면 스프링은 사용자가 정의한 Repository 인터페이스를 구현하고 SimpleJpaRepository를 target으로 포함하는 Proxy를 동적으로 만들어준다.
또, 그 Proxy를 Bean으로 등록해주고 연관관계 설정이 필요한 곳에 주입도 해준다.

References

profile
기록을 통한 성장을

0개의 댓글