Programming

IoC (DI, Service Locator...)

AlwaysPr 2018. 9. 9. 20:57

IoC

Inverse of Control는 제어권을 개발자가 아닌 제 3자가 가지게 하는 것이다.

그렇다면 우리는 왜 제어권을 3자에게 위임해야 하는가?
이에 대한 답을 찾기전에 과거로 돌아가보자. 과거 많은 형태의 오픈소스들이 나오고 있었고, 이들의 공통적인 이슈는 서로 다른 객체를 어떻게 연결할 것인지에 대한 문제였다. 이를 해결할 한 가지 방법으로 IoC가 제시되었다.
즉, IoC의 주된 목적은 Application의 Dependency를 제거해서 느슨한 결합을 제공하는 것이다.


그럼 Dependency란 무엇일까?


Dependency (computer science) or coupling, a state in which one object uses a function of another object - Wiki

  • 코드에서 두 모듈 간의 연결.
  • 객체지향언어에서는 두 클래스 간의 관계


간단한 코드를 작성해보자.

public class MemberService {
    public String parseString(ObjectMapper objectMapper, Member member) throws JsonProcessingException {
        return objectMapper.writeValueAsString(member);
    }
}

위 코드는 Jackson 라이브러리의 ObjectMapper를 이용하여 특정 객체를 Json String으로 변환작업을 하는 로직이다.
MemberService는 ObjectMapper의 기능을 사용하고 있기 때문에 의존하고 있다고 할 수 있다.
ObjectMapper.writeValueAsString()의 구현부가 변하게 되면 MemberService.parseString() 또한 변하게 된다.


비슷한 개념인 Coupling이 있다 Coupling이란 모듈간의 결합도 및 상호의존성의 정도를 말한다.

결합도

위의 MemberService는 클래스간의 강하게 결합을 하고 있다.
왜냐하면 몇몇은 JSON 변환 작업을 ObjectMapper를 사용해서 그대로 재사용하면 되지만 다른 몇몇은 Gson을 사용하기 때문에 코드에 전반적인 수정이 필요하다.


이러한 강한 결합을 Interface의 도움을 받아 느슨하게 할 수 있다.

public interface JsonParser {
    <T> T parseObject(String s, Class<T> clazz);
    <T> String parseString(T obj);
}

public class MemberService {
    public String parseString(JsonParser jsonParser, Member member) {
        return this.jsonParser.parseString(member);
    }
}

이전 코드에서는 MemberService 클래스안에 ObjectMapper가 직접적으로 들어가있었지만, 이번 코드에서는 MemberService는 Interface인 JsonParser만을 알고 있다. 이로 인해 사용자는 원하는 JsonParser 구현체를 입맛에 맞게 사용할 수 있다.


또 다른 사례를 알아보자.

public class CalendarReader {
    public List readCalendarEvents(File calendarEventFile){
        //open InputStream from File and read calendar events.
    }
}

위의 코드는 XML Local file을 통해서 이벤트 목록을 읽어오는 메소드다. 본인만 쓴다면 문제가 없겠지만, 이 소스를 다수의 사람들이 사용을 해야한다. 그런데 그들 중 일부는 XML을 통해 이벤트를 관리하지만, 다른 몇몇은 DB, Network 등을 통해서 관리를 한다. 즉, 다른 리소스로 관리를 하는 사람은 해당코드를 재사용할 수가 없게 된다.


이를 좀 더 포괄적인 InputStream을 사용하면서 결합을 좀 더 느슨하게 유도 할 수 있다.

public class CalendarReader {
    public List readCalendarEvents(InputStream calendarEventFile){
        //read calendar events from InputStream
    }
}

이렇듯 느슨한 결합을 통하여 클래스의 재사용성을 높일 수 있다. 또한 재사용성을 높인 다는 말은 비슷한류의 중복코드가 제거될 수 있음을 의미하기도 한다.


자! 다시 처음으로 돌아가보자. IoC의 주된 목적은 Application의 Dependency를 제거하는 것이라고 하였다. IoC 방식에는 아래 사진외에도 여러가지가 있다 그러나 우리는 몇가지 핵심적인 방식들을 살펴보도록 하자.
사진

Dependency Injection

IoC 방식 중 가장 대표적인 방식으로 보인다. Interface의 느슨한 결합을 이용하여 Compile 시점에서 Dependency를 가지지 않고, Runtime 시점으로 미룰 수 있다.
이를 좀 더 쉽게 표현하자면, 코드상에서 구현체가 존재하지 않고 단지 Inteface만 존재한다. 이로 인해 구현부가 변경되더라도 해당 코드를 수정하는 것이 아닌 Dependency만 변경해 주면된다. 거두절미하고 코드를 살펴보자.

public class Member {
    private String name;
    private int age;
    private String address;
}

public class MemberService {
    private final ObjectMapper objectMapper = new ObjectMapper();

    public String parseString(Member member) throws JsonProcessingException {
        return this.objectMapper.writeValueAsString(member);
    }

    public Member parseObject(String member) throws IOException {
        return this.objectMapper.readValue(member, Member.class);
    }
}

위의 코드는 Member 객체를 JSON 형태의 String으로, JSON형태의 String을 Member객체로 변환하는 코드이다.


그런데 특정한 이슈(Library 지원 종료, 속도 문제, 회사 정책 등)로 인하여 JSON 변환 Library 인 ObjectMapper를 Gson이나 다른 라이브러리로 교체하고 싶으면 어떻게 될까?

public class MemberService {
    private final Gson gson = new Gson();

    public String parseString(Member member) {
        return this.gson.toJson(member);
    }

    public Member parseObject(String member) {
        return this.gson.fromJson(member, Member.class)
    }
}

아예 새로운 코드가 되어버렸다. 클래스와 메소드 이름만 같지 모든 구현부가 바뀌어버렸다.


개인이 혼자 쓰는 프로젝트라면 상관이 없을 것이다. 그러나 오픈소스 또는 여러 기업에 팔아야되는 입장인데 위처럼 구현부가 변할때마다 코드를 수정해서 줘야 한다면 큰 문제가 있다. 이를 우리가 사용자 입맛에 맞게 일일이 변경해서 주는 것이 아니라, 가이드를 제공해줌으로써 사용자가 알아서 입맛에 맞게 수정하도록 변경해보자.


앞서 말한 Interface의 도움을 받아 사용자에게 가이드를 줌과 동시에 객체간에 느슨한 결합을 맺어주자.

public interface JsonParser {
    <T> T parseObject(String s, Class<T> clazz);
    <T> String parseString(T obj);
}

public class MemberService {
    private JsonParser jsonParser;

    public String parseString(Member member) {
        return this.jsonParser.parseString(member);
    }
    
    public Object parseObject(String member) {
        return this.jsonParser.parseObject(member, Member.class);
    }
}

우리는 위처럼 코드를 작성 후 오픈소스로 공개를 하거나, 다른 기업에 팔면 된다.


그럼 ObjectMapper를 사용하는 기업은 어떻게 자기 입맛에 맞게 구현을 할까? 간단하다.

public class JacksonParser implements JsonParser {
    private final ObjectMapper objectMapper;

    public JacksonParser() {
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public <T> T parseObject(String s, Class<T> clazz) {
        try {
            return this.objectMapper.readValue(s, clazz);
        } catch (IOException e) {
            throw new JsonParseException(e);
        }
    }

    @Override
    public <T> String parseString(T obj) {
        try {
            return this.objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new JsonParseException(e);
        }
    }
}

다음은 Gson의 구현체이다.

public class GsonParser implements JsonParser {
    private final Gson gson;

    public GsonParser() {
        this.gson = new Gson();
    }

    @Override
    public <T> T parseObject(String s, Class<T> clazz) {
        return this.gson.fromJson(s, clazz);
    }

    @Override
    public <T> String parseString(T obj) {
        return this.gson.toJson(obj);
    }
}

자 그럼 MemberService를 실행시켜보자. 잘 돌아갈 것이다.
는 무슨 NullPointerException이 떨어질 것이다.
왜냐하면 전역변수(jsonParser)로 선언만 해놓았지 구현체를 할당하지 않았기 때문이다.


우리는 전역변수에 인스턴스를 할당하는 방법을 잘 알고있다.
주로 우리는 다음과 같이 인스턴스를 할당한다.

public class MemberService {
    private JsonParser jsonParser = new JacksonParser();
}

public class MemberService {
    private final JsonParser jsonParser;

    public MemberSservice() {
        this.jsonParser = new JacksonParser();
    }
}

위의 코드의 문제점은 무엇일까?
우리는 지금까지 코드 레벨에서 특정 구현 객체(JacksonParser)를 보이지 않게 숨기려고 했는데, 다시 드러났다. 결국 허사가 된 것이다.


이를 다시 숨기려면 어떻게 해야 될까?
객체 생성을 사용자에게 전가시키고 그 객체를 주입을 받는 것이다. 좀 더 정확하게 말하자면 의존성을(Dependency)을 사용자에 의해 주입(Injection)받는 것이다.

Constructor Injection

주로 필수적인 Dependency에 사용된다.

public class MemberService {
    private final JsonParser jsonParser;

    public MemberService(JsonParser jsonParser) {
        this.jsonParser = jsonParser;
    }
}

Setter Injection

주로 부수적인 Dependency에 사용된다.

public class MemberService {
    private JsonParser jsonParser;

    public void setJsonParser(JsonParser jsonParser) {
        this.jsonParser = jsonParser;
    }
}

Method Injection

Setter Injection과 비슷하므로 생략한다.

위의 3가지 경우 중 하나로 구현을 했으면, 사용자는 다음과 같이 사용하면 된다.

public static void main(String [] args) {
    JsonParser parser = new JacksonParser();

    //Constructor Injection
    MemberService memberService = new MemberService(parser);

    //Setter Injection
    memberService = new MemberService();
    memberService.setParser(parser);

    memberService.parseObject(...);
    memberService.parseString(...);
}

또한 유닛 테스트를 좀 더 쉽게 할 수 있는 장점이 있다.
유닛 테스트는 일반적으로 외부의 의존성을 제외하고, 해당클래스에 집중을 하는 테스트 기법이다.
MemberService의 경우에는 사실 비즈니스 로직이 없이 의존성을 가진 인스턴스의 기능을 사용하는 것 뿐이지만, 로직이 있다고 가정을 하고 작성을 해보자.
JsonParser의 구현체들이 직접 실행되는 것이 아닌 Mock, Stub의 개념을 조금 넣어보자. 해당코드는 아래와 같다.

public class MockJsonParser implements JsonParser {
    @Override
    public <T> T parseObject(String s, Class<T> clazz) {
        return new Member("김민수", 26, "수원시");
    }

    @Override
    public <T> String parseString(T obj) {
        return "{\"name\" : \"김민수\", \"age\" : 26, \"address\" : \"수원시\"}";
    }
}

public static void main(String [] args) {
    JsonParser parser = new MockJsonParser();

    //Constructor Injection
    MemberService memberService = new MemberService(parser);

    memberService.parseObject(...);
    memberService.parseString(...);
}

이처럼 주입을 시켜주면 간단하게 테스트를 할 수가 있다.
사실 이 상황에서는 강력함이 보이지 않지만, 만약 이것이 JSON 변환 작업이 아닌 DB나 Network와 연결이 된 작업이라면 직접 해당 리소스와 연결되지 않고 Interface를 구현한 Mock객체로 간단하게 테스트를 해볼 수가 있다.


우리는 지금까지 사용자에게 Dependency Injection을 하게끔 유도함으로 유연하고 재활용가능한 클래스를 만들었다.


좀 더 생각해볼만 한 것은 스프링 레퍼런스와, 마틴파울러의 글에서는 IoC와 DI를 마치 동일하다는 듯이 설명을 해놓았다.

IoC is also known as dependency injection (DI) - Sping Reference

As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection - Martin Fowler

이 때문에 필자는 처음에 IoC와 DI가 동일한 줄 알았다. 그러나 아까 봤던 그림처럼 IoC에는 여러가지 구현 방법이 존재한다. 개인적인 추측으로는 많은 경우에서 IoC를 DI로 구현하기 때문에 위처럼 말한 것으로 보인다.

Service Locator

This process is fundamentally the inverse, hence the name Inversion of Control (IoC), of the bean itself controlling the instantiation or location of its dependencies by using direct construction of classes, or a mechanism such as the Service Locator pattern - Spring Reference

Service Locator에 관한 핵심만 말하자면 이를 이용해서도 제어를 역전(IoC)시킬 수 있다.


이 패턴 또한 목적은 Dependency를 제거하는 것이다. 그리고 DI와 비슷한 점이 많아 이해하기가 한결 쉬울 것이다. 구현된 코드를 한번 살펴보도록 하자.

public class MemberService {
    
    private final JsonParser jsonParser;

    public MemberService() {
        this.jsonParser = ServiceLocator.jsonParser();
    }

    public String parseString(Member member) {
        return this.jsonParser.parseString(member);
    }
}

public class ServiceLocator {
    public static JsonParser jsonParser() {
        //경우에 따라 Singleton이나 다른 Scope로 구현을 하기도 한다.
        return new JacksonParser();
    }
}

언듯보면 생성자를 통해 의존성을 주입하는 방식과 비슷해 보이기도 한다.
그러나 위에 작성한 Constructor Injection의 실행 코드를 보면 main() 메소드에서 사용자가 직접 new 키워드를 통해 인스턴스를 생성 후 주입을 해준다. 다시말하면 런타임시에 수동적으로 의존성이 연결이 된다.


그러나 Service Locator는 ServiceLocator.jsonParser()에 원하는 인스턴스를 생성해두면 MemberService가 생성이 될 때 직접 ServiceLocator.jsonParser()를 호출하여 능동적으로 의존성을 맺는다.


능동적이란 단어가 좀 긍정적여 보이긴 하지만, 위에서 처럼 테스트코드로 디펜던시를 바꿔야되는 상황을 한번 가정해보자.

public class ServiceLocator {
    public static JsonParser jsonParser() {
        //경우에 따라 Singleton이나 다른 Scope로 구현을 하기도 한다.
        return new MockJsonParser();
    }
}

그럼 ServiceLocator는 테스트할 때와 서비스를할 때의 상황에 따라 코드를 바꿔줘야되는 이슈가 생긴다.


이외에도 안티패턴이라고 여겨지는 몇가지 상황이 있다고 한다.


참고