본문 바로가기
프로그래밍이야기/Java

[객체지향] OCP 적용 정리

by 사랑꾼이야 2022. 1. 23.
반응형

최근 진행하고 있는 미션에서 코드 리뷰를 통해서 OCP의 대해서 피드백을 받은적이 있습니다.

OCP를 적용하면서 진행하였던 내용을 정리하려고 합니다.

상황

현재 외부 프로젝트 준비를 위해서 플랫폼 개발을 진행하고 있습니다. 그 중 API가 하나 있습니다. 이 API는 Elasticsearch를 조회해서 가져온 데이터를 전달하고 있습니다.

사실, 이 API는 기존에는 Database에서 조회한 내용을 전달하였습니다. 하지만 Elasticsearch를 연동하면서 변경되었습니다.

현재 개발 시에는 Elasticsearch를 사용하고 있지만, 외부 프로젝트 상황에 따라서 다른 라이브러리도 변경될 수 있습니다.

문제점

  • 외부 라이브러리를 사용하는 부분에서 다른 라이브러리를 활용하여 다른 방식으로 변경될 수 있습니다.
  • 그렇게 되면 API가 전체적으로 변경되어야 하며, 변경된 코드는 영향을 줄 수 있습니다.
  • Elasticsearch와 Database 연동을 모두 염두해주고 소스를 작성한다면, 너무 많은 코드로 인하여 복잡도가 증가되면서 기능을 추가하기가 어려운 구조가 됩니다.

결론

결국, 유지보수하기 힘든 코드가 되어버립니다.

이와 같은 상황을 해결하기 위해서 시간이 지나도 유지보수와 확장이 쉬운 시스템을 만들기 위해서 클린 코드의 저자 로버트 마틴객체지향 설계 5대 원칙을 만들었습니다. 그 원칙 중 OCP에 대해서 설명하겠습니다.

OCP

  • Open Closed Principle

개념

  • 확장에 대해서는 개방(OPEN)되어야 하지만 변경에 대해서는 폐쇄되어야 한다
  • 기존의 코드를 변경하지 않으면서 시스템을 확장할 수 있어야 한다는 의미입니다.

적용 방법

  • 방법에는 상속과 컴포지션이 있지만 상속의 단점을 고려하였을 때, 기존 소스에 영향을 주는 부분들이 있어서 컴포지션으로 진행하였습니다.
  • 컴포지션 진행
    • 변경이 필요한 부분과 변경이 필요하지 않는 부분으로 나눕니다.
    • 두 부분이 만나는 지점에 인터페이스를 정의합니다.
    • 이 인터페이스를 의존하게 만듭니다.

전략 패턴

  • OCP를 준수하기 위한 패턴
  • 전략을 쉽게 변경할 수 있도록 해주는 디자인 패턴으로 행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴입니다.
  • 가장 큰 장점은 새로운 기능의 추가가 기존의 코드에 영향을 미치지 못하게 되면서 OCP를 만족하게 됩니다.

변경이 필요한 부분과 변경이 필요하지 않는 부분 인터페이스 생성

class diagram

  • 변경이 필요한 부분을 TranHistoryStrategy 인터페이스로 분리하였습니다.
@Service
public class UserAccountCreditService {
    private final TranHistoryStrategy tranHistoryStrategy;  // 인터페이스
    ...
    ...
    public UserTranDto.Response tranHistroy(final String userId, final UserTranDto.Request request, final PageRequest pageable) {
                // 로직
        ...
                // 인터페이스의 의존
        final List<UserTranDto.UserTran> userTranList = tranHistoryStrategy.findTranHistory(mockUserId, request);
        ...
        return userTranList;
    }
}
  • UserAccountCreditServiceTranHistoryStrategy 인터페이스에 의존하도록 합니다.
@Component
public interface TranHistoryStrategy {
    List<UserTranDto.UserTran> findTranHistory(final String userId, final UserTranDto.Request request);
}
  • 빈 주입을 위해 @Component를 이용하였습니다.
@Component
public class ElasticsearchTranHistoryFinder implements TranHistoryStrategy {
    private final ElasticsearchConfig elasticsearchConfig;
    public ElasticsearchTranHistoryFinder(final ElasticsearchConfig elasticsearchConfig) {
        this.elasticsearchConfig = elasticsearchConfig;
    }

    @Override
    public List<UserTranDto.UserTran> findTranHistory(final String userId, final UserTranDto.Request request) {
                // 구현 로직
        final List<UserTranDto.UserTran> userTranList = new ArrayList<>();
        ...
        return userTranList;
    }
}
  • 빈 주입을 위해 @Component를 이용하였습니다.
  • 인터페이스를 이용하여 구현 클래스를 정의하였습니다.

마무리

  • 역할과 구현을 분리해야 합니다.
    • 역할 : 조회 내용 제공 역할 (TranHistoryStrategy)
    • 구현 : Elasticsearchㅇ 연동 및 조회 구현
  • 위와 같이 분리한다면 기존의 코드를 변경하지 않고 기능을 추가할 수 있습니다.
  • 전략패턴을 사용하게 된다면 인터페이스를 이용하여 테스트를 하기 어려운 부분도 용이하게 할 수 있습니다.
  • 하지만 인터페이스를 도입한다면 추상화라는 비용이 발생합니다.
  • 기능을 확장할 가능성이 없다면, 구현체 클래스를 직접 사용합니다.
  • 향후 꼭 필요하다면, 리팩토링해서 인터페이스를 도입하는 것이 방법입니다.
반응형

댓글