6. 그레이들 멀티프로젝트 - 데이터베이스 셋업 및 활용(2) - Functional Interface, dynamic bean selector
[배경]
세 가지 탈것의 종류가 있다; 자동차, 트럭, 자전거
각각의 고객은 이 세가지의 탈 것 중에 하나를 고를 수 있다.
자동차를 고른 고객은 car instance가 생성되어 프로그램에 사용되게 되고
트럭을 고른 고객은 truck instance가 생성되어 프로그램이 진행된다.
각각의 탈 것 instance에 따라서 속도, 무게, 부피 등등을 다르게 할 수 있다.
이렇게 하면, code base change없이 데이터베이스 값 변경만으로 dynamic한 request call이 가능하다.
1. 모델 클래스 준비
2. 탈것들의 Bean Config 만들기
3. 프로그램 시작시에 설정 불러오기
4. 테스트
1. 모델 클래스 준비
자동차, 트럭, 자전거의 세 가지 탈것 모두 공통점이 있으니 탈 것이라는 인터페이스로 묶기
public static create method로 interface를 통해서 instance 관리하자.
package com.example.monorepo.vehicle;
public interface Vehicle {
boolean canLoadPassenger(int passengerConsumption);
boolean canLoadGoods(int goodsConsumption);
boolean canDrive();
}
package com.example.monorepo.vehicle;
import lombok.Data;
@Data
public class Car implements Vehicle {
private final int passengerCapacityLimit = 4; // person
private final int goodsCapacityLimit = 100; // kg
int passengerCapacityCurrent = 0;
int goodsCapacityCurrent = 0;
@Override
public boolean canLoadPassenger(int passengerConsumption) {
if (passengerCapacityCurrent + passengerConsumption > passengerCapacityLimit) return false;
passengerCapacityCurrent += passengerConsumption;
return true;
}
@Override
public boolean canLoadGoods(int goodsConsumption) {
if (goodsCapacityCurrent + goodsConsumption > goodsCapacityLimit) return false;
goodsCapacityCurrent += goodsConsumption;
return true;
}
@Override
public boolean canDrive() {
return (passengerCapacityCurrent * goodsCapacityCurrent > 250) ? false : true;
}
public static Car create(){
return new Car();
}
}
package com.example.monorepo.vehicle;
import lombok.Data;
@Data
public class Truck implements Vehicle {
private final int passengerCapacityLimit = 2; // person
private final int goodsCapacityLimit = 500; // kg
int passengerCapacityCurrent = 0;
int goodsCapacityCurrent = 0;
@Override
public boolean canLoadPassenger(int passengerConsumption) {
if (passengerCapacityCurrent + passengerConsumption > passengerCapacityLimit) return false;
passengerCapacityCurrent += passengerConsumption;
return true;
}
@Override
public boolean canLoadGoods(int goodsConsumption) {
if (goodsCapacityCurrent + goodsConsumption > goodsCapacityLimit) return false;
goodsCapacityCurrent += goodsConsumption;
return true;
}
@Override
public boolean canDrive() {
return (passengerCapacityCurrent * goodsCapacityCurrent > 950) ? false : true;
}
public static Truck create(){
return new Truck();
}
}
package com.example.monorepo.vehicle;
import lombok.Data;
@Data
public class Bicycle implements Vehicle {
private final int passengerCapacityLimit = 1; // person
private final int goodsCapacityLimit = 20; // kg
int passengerCapacityCurrent = 0;
int goodsCapacityCurrent = 0;
public boolean canLoadPassenger(int passengerConsumption) {
if (passengerCapacityCurrent + passengerConsumption > passengerCapacityLimit) return false;
passengerCapacityCurrent += passengerConsumption;
return true;
}
public boolean canLoadGoods(int goodsConsumption) {
if (goodsCapacityCurrent + goodsConsumption > goodsCapacityLimit) return false;
goodsCapacityCurrent += goodsConsumption;
return true;
}
public boolean canDrive() {
return (passengerCapacityCurrent * goodsCapacityCurrent > 20) ? false : true;
}
public static Bicycle create(){
return new Bicycle();
}
}
2. 탈 것들 Bean Config 만들기
Functional Interface를 통해서 Vehicle클래스를 리턴한다.
Bean Config을 만들고, Primary설정
@FunctionalInterface
public interface VehicleManagerConfiguration {
Vehicle getVehicle();
}
@Configuration
public class VehicleManagerBeanConfig {
@Primary
@Bean("car")
VehicleManagerConfiguration carManager() {
return Car::create;
}
@Bean("truck")
VehicleManagerConfiguration truckManager() {
return Truck::create;
}
@Bean("bicycle")
VehicleManagerConfiguration bicycleManager() {
return Bicycle::create;
}
}
Vehicle클래스가 아닌 VehicleManagerConfiguration을 리턴하는 이유는, Bean의 리턴타입이 functional interface이어야 하기 때문. Additional wrapper class이다.
3. 프로그램 시작시에 설정 불러오기
ApplicationContext를 이용하면, 어플리케이션이 동작될때에 추가 로직을 실행할 수 있다.
여기에서는 데이터베이스 값(Client의 vehicle설정 값)을 불러와서 그 이름의 bean를 찾아 리턴하자.
다시 한번, Vehicle instance 아닌 Vehicle Functional Interface를 리턴한다.
@Component
public class VehicleSelector {
private final ApplicationContext applicationContext;
public VehicleSelector(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public VehicleManagerConfiguration determineVehicleForClient(Client client) {
Optional<String> vehicleBeanName = Optional.ofNullable(client.getMonoParameters().get(MonoParameter.VEHICLE));
Map<String, VehicleManagerConfiguration> beansOfType = applicationContext.getBeansOfType(VehicleManagerConfiguration.class);
return vehicleBeanName.map(x -> beansOfType.getOrDefault(x, applicationContext.getBean(VehicleManagerConfiguration.class)))
.orElse(applicationContext.getBean(VehicleManagerConfiguration.class));
}
}
4. 테스트
@Service
public class VehicleService {
@Autowired
private VehicleSelector vehicleSelector;
// ...
public Vehicle getMyVehicle(Client client){
return vehicleSelector.determineVehicleForClient(client).getVehicle();
}
}
@RestController
@RequestMapping(value = "/main")
public class MainController {
// ...
@Autowired
ClientService clientService;
@Autowired
VehicleService vehicleService;
// ...
@GetMapping("/vehicle")
public String getClientVehicle(@RequestParam("guid") String clientId){
Client client = clientService.getClientById(clientId);
return vehicleService.getMyVehicle(client).getClass().toString();
}
}
브라우저 열고 다음의 값을 실행(GET)
http://localhost:8080/main/vehicle?guid=cac6cea0-8620-4d0f-be62-dd24168a40c2
class com.example.monorepo.vehicle.Car
http://localhost:8080/main/vehicle?guid=3f99ea1c-6c62-46ca-8e25-6f8ac34bb222
class com.example.monorepo.vehicle.Truck
http://localhost:8080/main/vehicle?guid=ff6fb705-b37a-415f-bcd5-7088936938e5
class com.example.monorepo.vehicle.Bicycle
완성된 코드