본문 바로가기

끄적끄적

[끄적끄적] JpaRepository 접근 프록시 계층 설계

[회고] JpaRepository 접근 프록시 계층 설계

토이 프로젝트를 진행하며 맞닥뜨린 설계문제를 객체지향적으로 해결한 경험을 작성한 글 입니다.

 

위 그림상에서 proxy 부분에 해당하는 설계다.

 

(아직 배포하지않은 프로젝트이기 때문에, 설계도내의 클래스 이름은 모두 A,B,C,D ... 로변경했습니다.)

 

최종 설계도

문제상황

토이 프로젝트를 진행하다가, DB와 통신하는(정확히는 JpaRepository와 통신함) 서비스 객체의 프록시를 만들어야 하는 상황이였다.

 

우선, 기존의 문제되었던 소스코드 설계도를 보자.

(쉽게 이해하기 위해, 이 글에 필요하지 않은 메소드들은 설계도상에서 모두  삭제했다.)

 

문제되었던 설계도

 

기존설계의 문제점은, 실제 코드상 에서 물리적으로 전혀 연관이 없는(연관이 있으면 안되는) 인터페이스 구현 클래스들 사이에 의도치않은 논리적인 의존성이 생긴다는 것 이였고, 이는 기능의 확장, 추가, 변경에 악 영향을 미쳤다.

 

예를 들어, CrudService 클래스들중 한 클래스에서 다른 타입의 매개변수를 받아야 하는 기능이 추가된다면, CrudProxy 인터페이스에는 Service클래스에 추가된 기능에 해당하는 메소드가 함께 추가 되어야 하며, CrudProxy인터페이스를 구현하고 있는 모든 클래스가 인터페이스 추가된 이 메소드를 Override 해야한다. (심지어 추가된 메소드가 전혀 사용되지 않더라도!)

이런 상황은 개발단계에서 자주 일어날수 있는데, DB와 통신하는 CrudService 객체는 목적에 따라 String 매개변수를 받아야 할 때도 있고, Long을 받아야 할 수도 있으며, 두개 이상의 매개변수가 필요할수도 있다. 유저의 이름(String)으로 DB에서 조회를 하고, DB에 저장된 id(Long)으로 DB에서 조회를 해야하는 상황이 있다고 가정해보자. 이 경우, CrudService 객체에는 Long타입을 매개변수로 받는 조회 매소드 하나, String타입을 매개변수로 받는 조회 메소드가 하나 필요하다. 프록시 인터페이스 에서도 이 두 메소드에 대응되는 메소드를 만들어야 하며, 이 프록시 인터페이스를 구현하는 모든 프록시 구현체가 두 메소드를 추가로 정의해야한다.

 

위의 문제되는 설계를 해결해보자.

리팩토링으로 얻고자 하는 목표

1. 인터페이스 구현 클래스들간의 의도치않은 논리적 의존성을 없앤다.

 

2. 인터페이스 구현 클래스의 기능 기능추가 삭제 변경은 독립적으로 이루어져야한다.

(다른 구현클래스에 영향을 미쳐서는 안되고, 기능 추가를 위해 다른 클래스를 알 필요도 없어야 한다.)

 

3. 2번과 마찬가지로 인터페이스가 대변하는 서비스 객체(CrudService객체)의 기능추가, 삭제, 변경 또한 독립적으로 이루어져야한다. 자신을 대변하는 프록시 객체에만 영향을 미쳐야하며, 다른 프록시 객체에는 의도치않은 영향을 미쳐서는 안된다.

 

4. 클라이언트는 통신시 인터페이스의 구현 클래스를 알 필요 없다.

 

설계 아이디어 : CrudProxy구현체들은 각자 하나의 매개변수에 대한 처리를 담당하고, 이 CrudProxy구현체들을 서로 연결하여, 사용자가 전달한 매개변수의 타입이 자신이 처리할 타입이 아니라면, 자신과 연결된 CrudProxy구현체에 요청을 전달한다. (자신이 메시지를 처리할때까지 메소드 연쇄가 일어난다고 생각하면 된다.)

 

우선 구현될 최종 설계도를 보자.

(설계도에 대한 설명은 설계도 2 아래에 있다.)

(제네릭은 유연성과 타입안정성을 늘리기 위한 선택사항으로 설계의 핵심은 아니니 무시하고 봐도 무방하다.)

 

설계도 1

 

다음으로 위 구현의 뼈대가 되는 설계도를 보자.

설계도 2

CrudProxy인터페이스를 구현하는 한 객체는 오직 하나의 매개변수에 대한 처리를 담당한다. 예를들어,

StringParameterCrudProxy는 String매개변수에 대한 처리를 담당하며, LongParameterCrudProxy는 Long매개변수에 대한 처리를 담당한다. 2개 이상의 파라미터를 받는 경우도 마찬가지로 StringLongParameterCrudProxy(이 구현체는 순서대로 String, Long파라미터를 받는다.)로 작성했다.

 

설계도 1을 보면, 이 CrudProxy인터페이스가 책임 연쇄 패턴과 유사한것을 알수있으며, 클래스 연쇄 생성은 추상 팩토리를 사용하여 클라이언트의 잘못된 연쇄 사용을 방지했다.

 

실제 구현 소스코드를 보자.

CrudProxy

public interface CrudProxy<V extends Object>{
	
	V create(Object ...args);
	
	V read(Object ...args);
	
	V update(Object ...args);
	
	void delete(Object ...args);
	
	void addProxy(CrudProxy<V> crudProxy);
	
}

 

클라이언트는 이 구현체와 소통한다.

 

CrudProxy 구현체

UserDTO파라미터에 대한 처리를 담당한다.

@Service
public class ACrudProxy implements CrudProxy<UserDTO>{
	
	private CrudProxy<UserDTO> crudProxy;
	private final AService aService;
	
	@Override
	@Transactional
	public UserDTO create(Object ...args){
		if(args.length==1 && args[0].getClass().equals(UserDTO.class)) return aService.save((UserDTO)args[0]);
		return this.crudProxy.create(args);
	}
	
	@Override
	@Transactional(readOnly=true)
	public UserDTO read(Object ...args){
		return this.crudProxy.read(args);
	}
	
	@Override
	@Transactional
	public UserDTO update(Object ...args){
		if(args.length==1 && args[0].getClass().equals(UserDTO.class)) aService.edit((UserDTO)args[0]);
		return this.crudProxy.update(args);
	}
	
	@Override
	@Transactional
	public void delete(Object ...args){
		this.crudProxy.delete(args);
	}
	
	@Override
	public void addProxy(CrudProxy<UserDTO> crudProxy){
		if(this.crudProxy == null) this.crudProxy = crudProxy;
		else this.crudProxy.addProxy(crudProxy);
	}
	
	@Autowired
	public ACrudProxy(AService aService){
		this.aService = aService;
	}
	
}

 

CrudProxy 구현체 2

String 파라미터에 대한 처리를 담당한다.

@Service
public class AStringCrudProxy implements CrudProxy<UserDTO>{
	
	private CrudProxy<UserDTO> crudProxy = null;
	private final AService aService;
	
	@Override
	public UserDTO create(Object ...args){
		return this.crudProxy.create(args);
	}
	
	@Override
	public UserDTO read(Object ...args){
		if(args.length==1 && args[0].getClass().equals(String.class)) return aService.get((String)args[0]);
		return this.crudProxy.read(args);
	}
	
	@Override
	public UserDTO update(Object ...args){
		return this.crudProxy.update(args);
	}
	
	@Override
	public void delete(Object ...args){
		if(args.length==1 && args[0].getClass().equals(String.class)) aService.delete((String)args[0]);
		else this.crudProxy.delete(args);
	}
	
	@Override
	public void addProxy(CrudProxy<UserDTO> crudProxy){
		if(this.crudProxy == null) this.crudProxy = crudProxy;
		else this.crudProxy.addProxy(crudProxy);
	}
	
	@Autowired
	public AStringCrudProxy(AService aService){
		this.aService = aService;
	}
	
}

 

CrudProxy의 객체군을 생성하는 추상팩토리 인터페이스

public interface CrudFactory<V> {
	
	CrudProxy<V> get();
	
}

 

A 집합군을 생성후 반환하는 추상팩토리 인터페이스 구현 클래스

@Service
public class ACrudFactory implements CrudFactory<UserDTO>{
	
	private final CrudProxy<UserDTO> crudProxy;
	
	@Override
	public CrudProxy<UserDTO> get(){
		return this.crudProxy;
	}
	
	@Autowired
	public ACrudFactory(@Qualifier("aCrudProxy") CrudProxy<UserDTO> aCrudProxy,
    			    @Qualifier("aStringCrudProxy") CrudProxy<UserDTO> aStringCrudProxy){
		this.crudProxy = aCrudProxy;
		this.crudProxy.addProxy(aStringCrudProxy);
	}
	
}

 

CrudProxy를 사용하는 클라이언트 코드

public class AClient {
	
	private final CrudProxy<UserDTO> aCrudProxy;
	
	/.../
    
	@Autowired
	public AClient(@Qualifier("aCrudFactory") CrudFactory<UserDTO> aCrudFactory){
		this.aCrudProxy = aCrudFactory.get();
	}
}

프로젝트 링크

https://github.com/gitofolio/gitofolio 

 

GitHub - gitofolio/gitofolio: 💎 Github, Notion, Blog... 를 자신의 이력 카드로 꾸미세요

💎 Github, Notion, Blog... 를 자신의 이력 카드로 꾸미세요. Contribute to gitofolio/gitofolio development by creating an account on GitHub.

github.com