Java

[객체지향설계원칙] OCP

shininghyunho 2024. 5. 23. 12:51

OCP

Open Closed Principle.

(새로운 코드 추가) 열려있고, (기존 코드 변경) 닫혀 있어야한다.

즉 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야한다.

이는 유지보수성과 확장성을 높이는데 핵심적인 역할을 합니다.

 

OCP를 적용하기 위해선 추상화 다형성이 필수적입니다.

왜냐하면 클래스마다의 의존성은 실제 클래스가 아닌,

인터페이스에만 의존을 해야하기 때문입니다.

 

예를들어 우리가 로그인 기능을 구현하려고합니다.

근데 직접 로그인 기능을 구현하는 대신 Oauth를 이용하려고합니다.

 

근데 각 SNS마다 로그인 과정이 조금씩 차이가 나서 구현하기 까다롭습니다.

그렇다면 어떻게 구현해야할까요?

 

다음과 같은 SNS 들이 있습니다.

public enum SNSType {
    FACEBOOK,
    GOOGLE,
    NAVER,
    KAKAO
}

 

 

그럼 Service 계층에서 이에 해당하는 기능들을 구현해주면 다음과 같습니다.

public class SNSLogin {
	public void login(int snsId, String accessToken) {
    	if(SNSType.FACEBOOK==SnsType.fromId(snsId)) {
        	// facebook 로그인 과정 ...
        }
        else if(SNSType.GOOGLE==SnsType.fromId(snsId)) {
        	// google 로그인 과정 ...
        }
        ...
    }
}

 

이때 로그인 과정이 단순하지 않을것입니다.

뭐 다른 클래스를 호출하고 DB 호출하고 복잡한 과정이 섞여있죠.

그런데 그 과정이 모두 같은 SNSLogin 클래스 login 메서드에서 이루어집니다.

 

또한 지금은 login 기능만있는데 logout 기능, 토큰 재발급, .. 등등의 기능들이 추가된다고하면

지금보다도 클래스는 더욱 커지고 이에따라 기능파악도 힘들어집니다.

 

Apple 로그인을 추가한다고 생각하면 위의 코드를 대략적으로 사람눈으로 검토하며

기능을 구현할것이고요.

 

그래서 모든 SNS 과정이 통합되고 획일화된 표준이 필요해졌습니다.

그 기능을 해주는것이 inferface입니다.

public interface OauthService {
    void login(String accessToken);
    // logout, tokenRefresh 등등 더 추가 가능!
}

 

 

이제 Oauth 기능을 만들고 싶으면 OauthService 에 내용들을 구현하기만하면 됩니다.

@Service
public class FacebookOauthService implements OauthService {

    @Override
    public void login(String accessToken) {
        // 페이스북 로그인 로직 구현
        System.out.println("페이스북 로그인: " + accessToken);
    }
}

// GoogleOauthService, NaverOauthService, KakaoOauthService 등 구현체 추가

 

 

이렇게 획일화된 서비스 내용은 수정과 변형이 쉽습니다.

먼저 Factory 클래스에서 호출하기 쉽게 만들어줍니다.

public class OauthServiceFactory {

    private final Map<SNSType, OauthService> oauthServiceMap = new HashMap<>();

    public OauthServiceFactory() {
        // OauthService 구현체들을 Map에 등록
        oauthServiceMap.put(SNSType.FACEBOOK, new FacebookOauthService());
        // ... (Google, Naver, Kakao 등 추가)
    }

    public OauthService getOauthService(SNSType snsType) {
        return oauthServiceMap.get(snsType);
    }
}

 

그리고 이를 메인 Service 클래스에서 호출해줍니다.

이때 의존성이 OauthService 인터페이스로만 되어있기에,

호출하는 메인 Service 클래스에서는 실제 구현체는 알지도 알필요도 없어지죠. 

@Service
public class SNSService {

    private final OauthServiceFactory oauthServiceFactory;

    public SNSService(OauthServiceFactory oauthServiceFactory) {
        this.oauthServiceFactory = oauthServiceFactory;
    }

    public void login(int snsId, String accessToken) {
        SNSType snsType = SNSType.valueOf(snsId);
        OauthService oauthService = oauthServiceFactory.getOauthService(snsType);
        oauthService.login(accessToken);
    }
}

 

결과적으로 의존성은

SNSService  <- OauthServiceFactory <- OauthService(Interface) <- FacebookOauthServcie(실제구현)

처럼 생깁니다.

 

 

(이 예제에서는 OauthServiceFactory는 편의를 위해 일반 클래스로 구현했지만

실제로는 이또한 인터페이스로 되어있습니다. Spring 같은 프레임워크를 보면 알수있죠.)

 

이 인터페이스는 웬만하면 변경하면 안됩니다.

왜냐하면 모두 이 인터페이스를 기반으로 구현되어있기에 이를 변경하면

구현체들을 모두 변경해야하기 때문입니다.

 

그래서 처음에 설계가 굉장히 중요합니다.

이를 나중에 수정하려면 구현이 다 된 상태에서는 힘드니까요.