Autowired Class가 null 일 때

Caesars·2022년 2월 21일
1

현상

얼마 전에 DAO 객체가 NullPointerException 에러를 내는 일이 있었습니다. Autowired 어노테이션이 설정되어 있어서 의존 관계 주입 문제는 아니라고 생각했는데 찾아보니 Controller에서 DAO 객체를 field로 가지는 상위 객체를 new 연산자로 생성해주고 있었습니다. 따라서 Spring Container의 구성 프로세스를 거치지 않아 DAO가 제대로 생성되지 않았었습니다. 과거에 비슷한 문제를 겪었는데 오랜만에 보니 해결 방법이 가물가물 했네요..

public class CampaignScenarioOne extends CampaignScenarioAbs<RequestBody> implements Consumer<RequestBody>{
	@Autowired
    CampaignDAO campaignDAO;
    
    @Override
    public void accept(RequestBody requestBody) {
    	// 대충 여기서 NullPointer 에러 발생
        Optional.ofNullable(campaignDAO.getRecommendOneGoodsCode())
        	.orElseThrow(() -> {
                // 대충 에러 처리
                });
        }
}

Controller 에서 CampaignScenarioAbs 클래스의 구현체를 field로 가지고 있지 않은 이유는 한 클래스 안에 필요로 하는 모든 기능을 구현하면 코드가 너무 방대해지기에 기능별로 클래스를 나눴습니다.

@Controller
public class Controller {    
	@Autowired
    CampaignScenarioAbs campaignScenarioOne;
    @Autowired
    CampaignScenarioAbs campaignScenarioTwo;
    @Autowired
    CampaignScenarioAbs campaignScenarioThree;
    ....//	수십개까지 가능
}

다만 이렇게 나눈 클래스의 종류는 차후 수십개까지도 늘어날 수 있기에 Controller에서 전부 field로 가지기에는 무리라고 생각이 들었습니다. 따라서 상황에 따라 필요한 객체를 생성했지만 DAO 객체의 DI문제가 생겼습니다.


enum ScenarioType {
	CAMPAIGN_COMMON_1("KC1", CampaignCommonOne.class, "MethodOne"),
    CAMPAIGN_COMMON_2("KC2", CampaignCommonTwo.class, "MethodTwo"), 
    
    private final String type;
    private final Class<?> campaignInfoClass;
    private final String method;

	public static ScenarioType find(String type) {
    	ScenarioType[] scenarioTypes = ScenarioType.values();
        
        for(ScenarioType scenarioType : scenarioTypes) {
        	if(type.equals(scenarioType.getType())) {
            	return scenarioType;
            }
        }

        throw new RuntimeException("존재하지 않는 시나리오 타입 입니다.");
    }
}

public void handleCampaign(){
   	// field로 가지는 대신에 new로 생성
    Consumer<RequestBody> consumer = 
    	CampaignScenarioAbs.factory(ScenarioType.find(RequestBody.getScenarioType()));
	consumer.accept(requestBody);
}

해결책

Bean을 수동으로 주입

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;   
    }

    public static ApplicationContext getContext() {
        return context;
    }
}
@Controller
public class Controller {    

    public void do() {
        CampaignScenarioOne scenario = ApplicationContextHolder.getContext().getBean(CampaignScenarioOne.class);
    }
}

ApplicationContext를 사용하여 간편하게 구현할 수 있는 방법입니다. 단순하게 IOC Container 에 있는 Bean들을 가져오는 방법을 제공하지만 되도록 지양하는 편입니다. 이유는 스프링의 핵심인 의존관계 주입(Dependency Injection)에 위배되기 때문 입니다. 따라서 해당 방법은 놔두고 다른 방법을 찾아보았습니다.

Controller에서 DAO 전달

@Controller
public class Controller {    
	@Autowired
    CampaignDAO campaignDAO;
    
    public void do(){
    Consumer<RequestBody> consumer = CampaignScenarioAbs.factory(ScenarioType.find(RequestBody.getScenarioType()), campaignDAO);	//생성시 dao 전달
	consumer.accept(requestBody);

}

Controller에서 객체 생성시 dao를 전달했습니다. 동작에는 별 문제가 없습니다.

다만 Controller에서 DAO를 field로 가지는 것이 맞나 의문이 들었습니다. 일반적인 Spring 구조를 생각했을때 Service에서 가지고 있기 마련이니까요. 팀원에게 조언을 구해보니 실질적으로 사용을 하는 것은 아니기에 큰 문제 없다고 생각한다고 하더군요. 뭐 동작은 하지만 좋은 방법은 아닌듯 합니다.


참조

https://stackoverflow.com/questions/19896870/why-is-my-spring-autowired-field-null

profile
잊기전에 저장

0개의 댓글