본문 바로가기

JAVA/Spring

[Spring] 의존성, 의존관계 주입(DI) 대체 무슨 말일까?

생짜 자바코드에서 스프링으로 넘어올 때 가장 정신이 혼미해지는 구간이다. 의존성? 의존관계? 주입? 이 단어의 나열부터가 상당히 난해하다. 스프링 컨테이너, IoC 컨테이너, 어노테이션까지는 확실히 기존 생짜 서블릿 코드보다 확실히 편해진것같은데 여전히 의존성 주입이라는 말이 와닿지 않는다. 모든 지식이 그러하듯, 막상 이해하면 별개 없다. 의존성, 의존관계 주입에 대해 알아보자.

 

보통의 스프링 프로젝트는 데이터를 컨트롤하는 레포지토리(매퍼, 다오 등), 클라이언트에게 던지는 서비스, 이를 클라이언트에게 연결해주는 컨트롤러로 구성 돼 있다. 샘플 레포지토리에서 끌어온 데이터를 서비스단에 던진다고 가정해보자.

 

빈 객체 및 레포지토리

예제의 편의상 VO 등은 생략한다.

public class SampleDTO {
	private Long id;
	private String name;
}
public interface SampleRepository {
	void save(SampleDTO sampleDTO);
}
public class SampleRepositoryImpl implements SampleRepository {
	
	private static HashMap<Long, SampleDTO> store = new HashMap<>();
    
	@Override
	public void save(SampleDTO sampleDTO){
		store.put(sampleDTO.getId(), sampleDTO);
	}
}

 

아이고 길다~ 예제엔 DB가 따로 없으니 해시맵을 스태틱으로 올린다.

 

서비스단

public interface SampleService {
	void register(SampleDTO sampleDTO);
}
public class SampleServiceImpl implements SampleService {

    private final SampleRepository sampleRepository = new SampleRepositoryImpl();

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

당연하게도 데이터를 끌어오려면 서비스단에서 레포지토리 인스턴스를 생성해야한다. 그런데 여기서 데이터를 컨트롤하는 레포지토리 로직을 일시적으로 바꿔야 할 일이 생겼다고 가정해보자. 물론 무식하게 레포지토리 구체가서 직접 코드를 수정하면 일시적으론 편할 수 있다. (나도 그러고싶다.) 좀 더 개발자적인 시각으로, 우리가 굳이 굳이 객체지향까지 쓰는 이유를 다시 한번 되새기자. 객체지향으로 설계된 프로젝트라면 그 의도에 맞게 객체를 부품처럼 갈아끼우면 될 것이다.

 

 

public class SampleServiceImpl implements SampleService {

    private final SampleRepository sampleRepository = new EventRepositoryImpl();

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

그야 구체를 새로 하나 만들고 갈아끼면되지! 라고 생각하며 SampleRepositoryImpl을 EventRepositoryImpl로 갈아끼웠다. 위 예제코드는 아주 아주 단순하게 작성되었으니 코드한 줄 바꾸면 된다고 생각할 수 있다. 그러나 프로젝트의 규모가 커지고 장기적으로 봤을 때, 로직, 정책등이 바뀔 때 마다, 바뀐건 레포지토리인데 서비스단까지 계속 바꿔야한다고 생각해보라. 보통 성가시러운 일이 아니다. 

 

이렇게 일단 어떤 클래스에서 다른 클래스를 끌어왔다면 위 클래스는 끌어온 클래스를 의존하는 상태이다. 게다가 위 서비스단은 레포지토리의 부품을 갈아끼우려면 서비스단까지 수정해야한다. 결합력이 굉장히 강한 상태이다. 그냥 레포지토리만 바꾸고 싶은데, 서비스단은 건들지 않는 방법은 없을까? 여기서 서비스와 레포지토리, 서로의 결합력을 낮추려면 어떻게 해야할까? 서비스단 내에서 부품 역할을 하는 레포지토리 구체를 언급조차 하지 않으면 된다.

 

public class SampleServiceImpl implements SampleService {

    private final SampleRepository sampleRepository;

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

구체 선언을 안하면 되지! 그냥 필드만 덜렁두고 테스트를 돌려보자. 당연하게도 널포인트익셉션이 뜰 것이다. 당연하다. 인스턴스명만 선언하고 인스턴스에 뭘 넣을지 정하지 않은 상태이다. 아니 구체를 언급도 하지 말라면서 그럼 어쩌란 말이냐~ 우리가 서비스단에서 구체를 언급하지 않는 동안에 다른 누군가가 알아서 해당 인스턴스명에 인스턴스를 생성해줘야한다. 유감스럽게도 생짜 자바코드로는 이를 해결할 수 없다. 그러나 다행이도 이를 해결해주는 것이 바로 스프링 프레임워크이다. 우리가 지겹게 들었던 IoC컨테이너, Bean객체, DI가 이를 해결해줄 것이다.

 

레포지토리단으로 돌아가보자.

 

@Component
public class SampleRepositoryImpl implements SampleRepository {
	
	private static HashMap<Long, SampleDTO> store = new HashMap<>();
    
	@Override
	public void save(SampleDTO sampleDTO){
		store.put(sampleDTO.getId(), sampleDTO);
	}
}

 

레포지토리의 구체에 @Component 어노테이션을 선언함으로써 위 클래스는 스프링 컨테이너에 들어간다. 물론 어노테이션만 붙인다고 모든게 해결되면 얼마나 편하겠냐만은 당연히 프로젝트에 스프링 설정을 따로 해줘야한다.

 

@Configuration
@ComponentScan(basePackages = "최상단 패키지")
public class AppConfig {
}

 

위의 예제처럼 스프링 설정 클래스를 만들고 위에 @Configuration 어노테이션을 붙이면 위 객체는 스프링 설정 객체가 된다. 또한 설정 어노테이션 아래 컴포넌트 스캔 어노테이션을 선언하면 프로젝트내 해당 패키지를 스캔하면서 모든 컴포넌트 객체를 스프링 컨테이너에 올린다. 

 

레포지토리 구체를 스프링 컨테이너에 올렸으니 다시 서비스단으로 돌아간다.

 

public class SampleServiceImpl implements SampleService{

    @Autowired
    private SampleRepository sampleRepository;

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

레포지토리의 기능을 할 구체를 언급하지 않고 스프링 컨테이너에서 꺼내오자. 이때 쓰는 어노테이션이 바로 @Autowired이다. 쓰라니까 무작정 쓰지만 굳이 써야할 이유를 납득하지 못했던 그 어노테이션이다. 이제 의존성, 의존성 주입이라는 말이 조금 와닿는다. 부품을 직접적으로 언급하지 않고도 인스턴스를 생성했다! (정확히는 스프링 컨테이너에서 끌어와 쓰는 것이다.) 이를 일컫어 의존성, 의존관계 주입이라 한다. 

 

그러나 유감스럽게도 이렇게 클라이언트에게 서비스되는 클래스 필드에 오토와이어드를 마구잡이로 써선안된다. 왜냐하면 테스트등에서 스프링없이 생짜 자바코드로 테스트를 돌리고 싶어도 스프링 없이는 돌아가지가 않기때문. (안되는건 아니지만 권장하지 않는다.) 때문에 이를 대체하기 위해 해당 클래스 생성자에 오토와이어드를 걸어준다.

 

public class SampleServiceImpl implements SampleService {

    private final SampleRepository sampleRepository;
    
    @Autowired
    public SampleServiceImpl(SampleRepository sampleRepository) {
    	this.sampleRepository = sampleRepository;
    }

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

레포지토리에 의존성 주입 완성! 그런데 뭔가 아쉽다. 코드가 쓸데 없이 길어보인다. 여기서 세상 모든 개발자들이 다 쓰는것같은 롬복 라이브러리를 붙인다. (롬복에 대한 설명은 따로 찾아보길 바란다.)

 

@Component
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {

    private final SampleRepository sampleRepository;

    @Override
    	public void register(SampleDTO sampleDTO){
    	sampleRepository.save(sampleDTO);
    }
}

 

여기서 롬복라이브러리의 @RequiredArgsConstructor 어노테이션은 final 키워드가 붙은 필드에 자동으로 생성자를 만들어준다. (같은 맥락으로 게터, 게터 등 만들기 위찮은 코드를 어노테이션만 붙이면 만들어주는게 롬복 라이브러리이다.) 여기서 왜 갑자기 컴포넌트를 붙였냐 하면 당연히 서비스단 역시 컨트롤러단에서 끌어다 써야하기 때문에 스프링 컨테이너에 올린것이다. 롬복까지 쓰므로써 아주 깔끔한 의존관계 주입 완성!

 

추가적으로, 그럼 구체를 직접적으로 언급하지 않기위해서 스프링을 쓰는거냐? 하면 것도 맞지만 생각보다 스프링 프레임워크는 더 엄청난 기능을 제공한다. 지금이야 공부하는 단계니 프로젝트를 만들어도 나와 팀원들 끽해야 10명 안팎만 들락날락거릴것이다. 허나 프로젝트가 배포되고 수천 수만이 쓰는 서비스가 된다면? 그때마다 프로젝트 내부에선 인스턴스를 수천 수만개 찍어낼것이다. (물론 요즘은 컴퓨터가 좋으니 그정도는 거뜬히 견디겠다만) 이 때 인스턴스를 단 한개만 찍어내는 설계 방법을 싱글톤 패턴이라 일컫는다. 다른 클래스를 끌어다 쓸 때마다 싱글톤 패턴을 적용하는건 상당히 귀찮은 일이다. (클래스 내부에서 인스턴스를 스태틱으로 올리는 둥 귀찮은 작업이다. 코드는 생략한다.) 그런데 스프링 프레임워크는 객체를 스프링 프레임워크에 올려 싱글톤으로 객체를 생성한다. 즉, 일일히 싱글톤으로 객체를 생성할 필요가 없다는 얘기다. 

 

그외에도 그지같은 서블릿 파일을 어노테이션 하나로 정리해주는 등 엄청나게 방대한 기능을 제공하겠지만 아직 알못이기때문에 여기까지 정리하겠다. 위 개념은 김영한님 강의를 보고 정리한 것이다.

 

 

 

728x90