요약
레디스에 캐시를 저장할 때 도메인 엔티티 그대로 저장하니 연관관계 필드가 프록시로 저장되는 문제가 있어서, 레플렉션을 이용해 캐시모듈에서 DTO-엔티티 변환 메서드를 호출하도록 수정했습니다. 그래서 캐시를 저장할 땐 DTO로, 꺼내올 땐 엔티티로 꺼내오는 게 가능하도록 했습니다.
이 글은 이전 글인 DB와 캐시를 연계하는 캐시 모듈에 이어서 작성된 글입니다. DTO-Entity 변환 방법을 이해하는데에는 무리가 없겠지만, 이 코드가 캐시 모듈 내부에 작성되는 점을 먼저 인지하고 읽어주시면 이해가 편하시겠습니다.
레디스에 캐시를 저장할 때 도메인 엔티티 인스턴스를 그대로 저장하니, 아직 로딩되지 않은 연관관계 클래스가 프록시 클래스로 저장되는데, 캐시를 꺼내볼 때는 이미 영속성 컨텍스트를 벗어난 시점이기에 지연로딩이 불가한 문제가 있었습니다. 그리고 캐시가 도메인에 결합되는점이나 보안 문제가 염려되었습니다.
이 담배200 프로젝트에선 캐시에 접근할 때 제가 정의한 캐시 모듈을 사용합니다. 이 캐시모듈에선 키와 값 등 대부분의 인자 타입을 제네릭을 사용하고 있었습니다. DTO/Entity의 양방향 변환 메서드는 엔티티가 아닌 DTO (예시) 에 위치하게 됩니다.
public Dto(Entity entity) // entity를 받아 DTO를 만드는 생성자
public static toEntity(Dto dto) // dto를 받아 entity를 만드는 static 메서드
제네릭을 사용하는 캐시 모듈에서는 Entity와 매칭되는 DTO의 타입을 모르니 이 변환 메서드를 호출할 방법이 없었습니다. 변환 메서드를 정의한 인터페이스를 만들어 각 DTO가 이를 구현하자니, 각 DTO 내에서 static하게 구현할 수 없었고, non-static하게 구현하자니 변환메서드를 사용하기 위해 DTO를 인스턴스화해야하는 불편함이 있었습니다.
그래서 DtoConverter라는 클래스를 만들어, 여기에서 DTO와 엔티티 클래스를 입력받고 변환 메서드를 리플렉션으로 적절한 변환 메서드를 호출하는 방식을 사용했습니다.
public class DtoConverter {
// valueClass에서 entityClass를 인자로하는 생성자를 찾아 실행
public static <V, E> V toDto(E entity, Class<V> valueClass, Class<E> entityClass){
try {
V value = valueClass.getDeclaredConstructor(entityClass).newInstance(entity);
return value;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
throw new UnhandledServerException("DTO에서 Entity를 인자로하는 생성자를 찾을 수 없습니다.");
}
}
public static <V, E> List<V> toDtoList(List<E> entities, Class<V> valueClass, Class<E> entityClass){
return entities.stream()
.map(entity -> toDto(entity, valueClass, entityClass))
.collect(Collectors.toList());
}
// valueClass에서 valueClass를 인자로하는 "toEntity"라는 이름의 메서드를 찾아 실행
public static <V, E> E toEntity(V value, Class<V> valueClass, Class<E> entityClass){
try {
E entity = (E) valueClass.getDeclaredMethod("toEntity", valueClass).invoke(null, value);
return entity;
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
throw new UnhandledServerException("DTO에서 Entity로 변환하는 'toEntity' 메서드를 찾을 수 없습니다.");
}
}
public static <V, E> List<E> toEntityList(List<V> values, Class<V> valueClass, Class<E> entityClass){
return values.stream()
.map(value -> toEntity(value, valueClass, entityClass))
.collect(Collectors.toList());
}
}
이 변환 로직을 캐시모듈 에 추가했습니다. 필요하다면 캐시를 저장할 땐 엔티티를 DTO로, 캐시를 불러올 땐 엔티티로 변환하게됩니다. (DTO 그대로 저장하거나 불러오는 메서드도 존재)
// 캐시, DB에 모두 저장
public <K, V, E> E writeThrough(CacheType cacheType
, K key
, E entity
, UnaryOperator<E> dbWriteFunction
, Class<V> valueClass
, Class<E> entityClass){
E saved = dbWriteFunction.apply(entity);
// 캐시에 저장하기 전에 Entity를 Dto로 변환
V dto = DtoConverter.toDto(saved, valueClass, entityClass);
put(cacheType, key, dto);
return saved;
}
엔티티와 1:1로 매핑되는 각 DTO는 무조건 변환 메서드를 구현해야 하는데, 각각 생성자와 static 메소드를 사용하기 때문에 구현을 강제하지 못합니다. 이게 어긋났음을 변환시점에야 알게되기 때문에 문제가 발생할 수 있습니다.
변환로직을 DTO가 아닌 변환 전용 클래스에 모아 두고 컴파일 시간에 @Entity가 붙은 모든 클래스의 이름에 대해 XXtoEntity, XXtoDTO 처럼 엔티티클래스명을 기반으로 변환 메서드를 작성하지 않으면 컴파일 오류가 나게끔 할 순 있을 것 같지만, 일단은 주의사항을 숙지하는 것으로 마무리합니다.
엔티티를 DTO로 변환함으로써 캐시를 좀 더 안정적이고 간결하게 사용할 수 있게 되었고 레플렉션을 직접 사용해 볼 수 있어서 의미 있던 리팩토링이었습니다.