스프링개발자/301 - 아키텍처

6. 그레이들 멀티프로젝트 - 데이터베이스 셋업 및 활용(2) - Functional Interface, dynamic bean selector

2ndPrince 2020. 10. 7. 05:51

[배경]

세 가지 탈것의 종류가 있다; 자동차, 트럭, 자전거

각각의 고객은 이 세가지의 탈 것 중에 하나를 고를 수 있다.

자동차를 고른 고객은 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

 

완성된 코드

github.com/2ndPrince/monorepo/tree/database-2