사이드 프로젝트를 진행하면서 본인인증 기능을 구현하기 위해 NICE API를 이용하기로 했다.
NICE API는 외부 시스템으로 식별하고 인프라 계층으로 분류했고, 애플리케이션 계층에서 외부 시스템으로 불러와서 사용하는 방식으로 구현해야한다.
하지만 고수준 모듈이 저수준 모듈에 의존하는 문제로 구현체 변경이 어렵다는 문제를 알게되었다.
저수준 모듈
: 구체적이고 상세한 작업을 처리고수준 모듈
: 저수준 듈보다 높은 수준의 추상화와 비즈니스 로직을 담당이 글에서는 위 문제를 해결했던 방법을 정리해보려고한다.
프로젝트에서 사용한 소프트웨어 아키텍처는 레이어드 아키텍처이고, 아래와 같다.
Presentation ( 표현 계층 )
↓
Application ( 응용 계층 )
↓
Domain ( 도메인 계층 )
↓
Infrastructure ( 인프라 계층 )
이처럼 위에서 아래로 단방향으로 흐르는 구조인 것을 볼 수 있다.
(레이어드 아키텍처에 대한 내용은 생략하도록 한다...)
개요에서 말했듯이 NICE API는 외부 시스템이고, 본인 인증 기능을 구현하기 위한 구현 코드는 인프라 계층에 속하게 된다.
이해하기 쉽게 애플리케이션 계층 코드로 알아보자.
@Service
@RequiredArgsConstructor
public class IdentityVerificationService {
private final NiceIdentifyVerficationClient client;
// 본인인증 팝업 호출을 위한 암호화된 인증 요청 데이터 생성
public EncryptedIdentityDataResponse requestEncryptedData() {
return client.requestEncrpytedData();
};
// 본인인증 후, 암호화된 응답 데이터 복호화 후 검증
public IdentifyVerificationResponse verifyIdentity() {
return client.verifyIdentity();
};
}
이처럼 애플리케이션 계층에서 외부 시스템에 접근하기 위해 Client 객체를 주입받아서 호출하는 구조이고, 애플리케이션 계층이 인프라 계층을 의존하는 상황이다.
기능은 정상 작동하지만 요구사항이 변경되어서 NICE API를 사용하지 않고 다른 외부 API를 사용한다고하면 아래와 같은 부분에서 컴파일 에러가 발생한다.
@Service
@RequiredArgsConstructor
public class IdentityVerificationService {
private final NiceIdentifyVerficationClient client; // 컴파일 에러
public EncryptedIdentityDataResponse requestEncryptedData() {
// 컴파일 에러
return client.requestEncrpytedData();
};
public IdentifyVerificationResponse verifyIdentity() {
// 컴파일 에러
return client.verifyIdentity();
};
}
이처럼 Nice API를 사용하는 코드 영역은 전부 컴파일 에러가 발생한다.
컴파일 에러가 발생한다는 것은 외부시스템을 의존하고 있다는 것이고, 외부 시스템이 변경되거나 장애가 생기면 우리 서비스도 장애가 생긴다는 것이다.
지금까지 문제점은 애플리케이션 계층이 외부시스템을 직접 의존하는 것이다.
위에서 말했듯이 애플리케이션 계층은 비즈니스 로직의 흐름을 나타내고, 인프라 계층은 외부 시스템에 대한 구현을 나타내는 계층이며 애플리케이션 계층이 고수준 모듈이고 인프라 계층이 저수준 모듈이 되는 것이다.
즉, 현재 상황은 DIP(의존성 역전 원칙)을 위반하고 있어 변경에 취약한 문제를 직면하게 된 것이다.
그렇다면 추상화 계층(인터페이스를)를 이용해 변경에 유연하도록 구현해보자.
현재 계층간 의존성은 시각화하면 아래와 같다.
Application : IdentifyVerificationService
↓
Infrastructure : NiceIdentifyVerificationClient
해결 방법은 간단하다. 의존성을 역전해주면 된다. 즉, 애플리케이션 계층 -> 인프라 계층
가 아니라 애플리케이션 계층 <- 인프라 계층
으로 만들어주면 된다.
Application : IdentifyVerificationService -> IdentityVerifier ( 본인인증 수행 객체를 의미하는 인터페이스)
↑
Infrastructure : NiceIdentifyVerificationClient
애플리케이션 계층에 추상화 객체(인터페이스)를 생성하고, 애플리케이션 계층 서비스가 인터페이스를 의존하도록 구현하면 된다.
그리고 외부시스템 구현체는 추상화 객체(인터페이스)를 구현하게 되면 의존성이 역전되어서 저수준 모듈이 고수준 모듈을 의존하게 되는 것이다.
의존 관계를 코드로 나타내면 아래와 같다.
// 새로 생성한 추상화 객체
public interface IdentityVerifier {
EncryptedIdentityDataResponse requestEncryptedData();
IdentifyVerificationResponse verifyIdentity()
}
// 추상화 객체를 의존하는 애플리케이션 서비스 코드
@Service
@RequiredArgsConstructor
public class IdentityVerificationService {
private final IdentityVerifier identityVerifier;
public EncryptedIdentityDataResponse requestEncryptedData(...) {
return identityVerifier.requestEncrpytedData(...);
};
public IdentifyVerificationResponse verifyIdentity(...) {
return identityVerifier.verifyIdentity(...);
};
}
@Component
@RequiredArgsConstructor
public class NiceIdentifyVerficationClient implements IdentityVerifier {
private NiceApiClient client;
public EncryptedIdentityDataResponse requestEncryptedData() {
return new EncryptedIdentityDataResponse(client.apiCall());
}
public IdentifyVerificationResponse verifyIdentity() {
return new IdentifyVerificationResponse(client.apiCall());
}
}
그러면 실제로 구현체를 변경해보는 예시를 들어보자.
@Component
@RequiredArgsConstructor
public class CustomVerficationClient implements IdentityVerifier {
private CustomApiClient client;
public EncryptedIdentityDataResponse requestEncryptedData() {
return new EncryptedIdentityDataResponse(client.apiCall());
}
public IdentifyVerificationResponse verifyIdentity() {
return new IdentifyVerificationResponse(client.apiCall());
}
}
위 코드는 가상의 CustomApiClient
를 호출해서 인증기능을 제공하는 CustomVerificationClient
구현체이다.
이 경우 애플리케이션 계층 코드는 인터페이스의 의존하기 때문에 구현체 변경에 영향을 받지 않는다. 즉, 구현체 코드만 새로 추가하고 Spring Bean으로 등록만하면 Spring IOC 컨테이너가 자동으로 구현체를 주입해주기 때문에 변경에 매우 강한 구조가 되는 것이다.
실제로 본인인증 기능에 대한 외부 API가 변동이 안될 수도 있다고 생각했다. 그렇기 때문에 오히려 코드만 복잡해질 수 있다고 생각했다.
하지만 소프트웨어 아키텍처의 원칙을 지키고 애플리케이션 계층과 인프라 계층 간 책임을 명확히 분리 하는 것이 올바른 선택이라고 생각했습니다.
그리고 테스트를 수행하는데 있어서도 독립적으로 테스트를 할 수 있다고 생각했습니다.
이처럼 현재 상황을 고려하기 보다는 요구사항은 항상 변할 수 있고 확장성에 유리하도록 설계하는 것이 올바른 선택이라고 생각하여 인터페이스를 이용해 애플리케이션 계층과 인프라 계층의 책임을 분리하도록 구현해보았습니다.