관심사의 분리는 객체지향적 프로그래밍을 위한 매우 중요한 개념이다.

또한 관심사의 분리는 스프링의 탄생배경과도 연관이 있기 때문에 한번쯤 정리하고 넘어가는것이 좋을거 같다.

말보다는 코드를 보면서 설명하는것이 효율적일거 같다. 우선 관심사의 분리가 일어나지 않은 코드를 살펴보자.

 

Member 객체를 저장하거나 조회하기 위해 사용되는 MemberRepository 인터페이스와 그것을 구현하는 두 종류의 구현체가 있다.

여기서는 각 객체들의 관계를 중심으로 설명할 것이므로 비지니스 로직이 중요한것이 아니여서 구현 코드는 생략하였다.

public interface MemberRepository{
    // Member객체 저장
    void save(Member member);
    // memberId를 이용하여 Member객체 조회
    Member findById(Long memberId);
}
// in-memory 파일시스템을 기반으로 작동한다고 가정
public class MemroyMemberRepository implements MemberRepository{
    @Overrid
    public void save(Member member){
        ...
    }
    
    @Override
    public Member findById(Long memberId){
        ...
    }
}
// 데이터베이스를 기반으로 작동한다고 가정
public class DBMemberRepository implements MemberRepsoitory{
    @Override
    public void save(Member member){
        ...
    }
    
    @Override
    public Member findById(Long memberId){
        ...
    }
}

 

다음으로 MemberRepository를 이용하여 회원가입, 회원조회와 같은 서비스기능을 제공하는 MemberService 인터페이스와 그 구현체이다.

public interface MemberService{
    // 등록
    void join(Member member);
    // 조회
    Member findMember(Long memberId);
}
public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}

 

자 이제 본격적으로 관심사의 분리에 대해서 이야기를 해보자.

현재 MemberService의 구현체인 MemberServiceImpl은 MemoryMemberRepsitory를 구현체로 하는 MemberRepository를 사용한다.

여기서 만약 MemberService를 데이터베이스 환경으로 구현하고 싶다면 우리는 MemberServiceImpl의 코드를 다음과 같이 변경할 것이다.

public class MemberServiceImpl implements MemberService{
    // MemberRepository의 구현체를 DBMemberRepository로 바꿈
    private final MemberRepository memberRepository = new DBMemberRepository();
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}

 

무엇이 잘못되었는지 감이 오는가?

감이 잘 오지 않는다면 객체지향 설계 5원칙 SOLID중에서 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), DIP(의존관계 역전 원칙)를 떠올려 보자.

 

SRP - "한 클래스는 하나의 책임만 가져야 한다"

현재 MemberServiceImpl은 의존 객체(MemberRepository)를 직접 생성하고 연결한뒤 로직을 구현하였다.

즉 "의존 객체의 생성", "연결", "로직 구현"이라는 3가지 책임을 동시에 맡고있다.  

 

OCP - "확장에는 열려 있으나 변경에는 닫혀 있어야 한다"

OCP 관점에서 우리는 interface를 통해 여러 구현체를 만듦으로써 확장을 할 수 있었다.

그렇다면 변경에는 닫혀 있는가?  대답부터 하자면 그렇지 않았다.

우리는 MemberServiceImpl에서 MemberRepository를 할당할때 상황이 달라질때마다 구현체를 직접 할당하는 방식으로 코드를 변경하여야 했다.

 

DIP - "추상화에 의존해야지, 구체화에 의존하면 안된다"

DIP관점에서 봤을때 MemberServiceImpl는 MemberRepository 인터페이스에 의존하는것 처럼 보이지만 실상은 그 구현체에 의존하고 있었다.

우리는 MemberServiceImpl를 구현할때 상황이 달라질때마다 MemoryMemberRepository를 직접 할당하기도, DBMemberRepository를 직접 할당하기도 했다.

 

이와 같은 현상이 벌어지는 이유는 기능구현의 책임을 가지고 있는 구현체가 의존관계 할당의 책임까지 맡고있기 때문이다.

따라서 이 같은 현상을 해결하기 위해서는 기능을 담당하는 구현체(MemberServiceImpl)는 역할구현에만 충실하고

서로의 의존관계를 배분하는, 즉 의존관계 주입을 담당하는 또 다른 무언가가 있어야 한다.

 

이를 위해서 우리는 client 외부에서 작동하는 의존관계 할당이라는 책임을 맡는 AppConfig라는 클래스를 만들어 보겠다.

public class AppConfig{
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

 

AppConfig를 활용하여 MemberServiceImpl를 리팩토링한 코드는 다음과 같다.

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    
    // 생성자를 통하여 MemberRepository의 구현체를 주입 받는다.
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}

 

이제 우리는 AppConfig를 통해 외부에서 역할을 부여받음으로써 내부 구현체는 기능구현에만 책임을 갖도록 하였다.

이러한 구조는 기존에 위반되었던 객체지향 설계원칙을 다음과 같은 이유로 지킬 수 있게 되었다.

 

SRP (단일 책임 원칙)

구현 객체의 생성 및 의존관계 주입은 AppConfig가 담당하고 구현 객체는 해당 기능을 구현하는 책임만 담당함으로써 단일 책임 원칙을 지켰다.

 

OCP (개방-폐쇄 원칙)

MemberService를 예를 들면, 해당 인터페이스의 다른 구현체를 추가적으로 만드는것이 가능하므로 얼마든지 확장 가능하다.

또한 구현체를 확장해 나간다 하더라도 의존관계에 대한 관리는 AppConfig에서 하므로 구현체 입장에서의 변경은 닫혀있다.

 

DIP (의존관계 역전 원칙)

MemberServiceImpl이 MemberRepository의 구현체에 의존하던것을 MemberRepository자체 인터페이스에만 의존하도록 바꿨다.

대신에 AppConfig가 MemberRepository의 구현체를 생성하여 MemberServiceImpl에 주입할 수 있도록 설계하였다.

 

 

정리해보자면 우리는 기존에 구현체에서 의존관계에 있는 객체를 직접 생성하고 맡은 기능을 수행하는등 여러가지 책임을 가지고 있었다.

하지만 기존 방식은 객체지향적 프로그래밍 법칙에 많은 부분을 위반하고 있었고 이를 보완하기 위해 AppConfig라는 외부장치를 만들었다.

AppConfig는 구현 객체들을 생성하고 연결시켜 주는 책임을 맡음으로써 기존 구현체들이 구현의 책임만 맡을 수 있도록 관심사의 분리를 제공하였다.

 

이렇게 외부에서 AppConfig가 프로그램에 대한 제어 흐름 권한을 가지고 구현 객체들을 생성 및 실행하는 경우 이것을 제어의 역전(IoC)라고 한다.

또한 AppConfig가 런타임 시점에 실행된다면 애플리케이션은 런타임에 구현 객체를 생성하고 의존관계를 연결하는데 이것을 의존관계 주입(DI)라고 한다.

즉 AppConfig는 IoC와 DI의 효과를 일으킨다고 말할 수 있다.

 

앞에서 관심사의 분리가 스프링의 탄생 배경과도 연관 있다고 말한 이유가 이 IoC와 DI에 있다.

스프링이 탄생하게 된 큰 이유중에 하나가 바로 IoC와 DI를 개발자들에게 좀 더 편하게 제공하려는 것이기 때문이다.

 

 

 

 

+ Recent posts