인터페이스 분리 원칙(ISP)이란 무엇인가요?

김상욱·2024년 11월 20일

인터페이스 분리 원칙(ISP)이란 무엇인가요?

인터페이스 분리 원칙(Interface Segregation Principle)은 객체 지향 설계의 SOLID 원칙 중 하나로 클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다는 것을 의미. 이는 인터페이스를 설계할 때, 클라이언트가 필요로 하는 기능만 제공하도록 작은 인터페이스로 나누어야 한다는 원칙.

  • 클래스가 자신과 관련 없는 기능을 가진 인터페이스를 구현하면, 불필요한 의존성이 생깁니다. 이는 유지보수를 어렵게 하고, 코드 수정 시 예상치 못한 오류를 초래할 수 있습니다.
  • 하나의 큰 인터페이스를 여러 개의 구체적이고 작게 분리하여, 클라이언트가 꼭 필요한 기능만 사용할 수 있도록 설계.
  • 인터페이스를 분리함으로써, 각각의 클라이언트가 자신에게 필요한 기능만 의존하게 됩니다. 이를 통해 코드의 재사용성과 확장성이 향상

ISP 위반

// 하나의 큰 인터페이스
public interface Worker {
    void work();
    void attendMeeting();
    void prepareReport();
}

// 모든 Worker가 이 인터페이스를 구현해야 함
public class Developer implements Worker {
    @Override
    public void work() {
        System.out.println("코드를 작성합니다.");
    }

    @Override
    public void attendMeeting() {
        // 불필요한 메서드
        System.out.println("회의 참석");
    }

    @Override
    public void prepareReport() {
        // 불필요한 메서드
        System.out.println("보고서 작성");
    }
}
  • Developer는 보고서 작성과 회의 참석 기능이 필요 없지만, 큰 인터페이스 때문에 모든 메서드를 구현해야 함. -> 불필요한 의존성 유발 및 유지보수 어려워짐

ISP 준수

// 하나의 큰 인터페이스
public interface Worker {
    void work();
    void attendMeeting();
    void prepareReport();
}

// 모든 Worker가 이 인터페이스를 구현해야 함
public class Developer implements Worker {
    @Override
    public void work() {
        System.out.println("코드를 작성합니다.");
    }

    @Override
    public void attendMeeting() {
        // 불필요한 메서드
        System.out.println("회의 참석");
    }

    @Override
    public void prepareReport() {
        // 불필요한 메서드
        System.out.println("보고서 작성");
    }
}
  • Developer는 작업에만 집중하고 Manager는 보고서 작성과 회의 참석에만 의존
  • 각 클래스는 필요한 인터페이스만 구현하므로 불필요한 의존성 제거

즉, 불필요한 코드 변경을 최소화하여 유지보수가 쉬워지고, 각 인터페이스가 독립적으로 설계되므로 재사용 가능성이 높아짐. 또한 작은 인터페이스로 나눔으로써 클래스가 결합도가 낮아짐. + 테스트 시에도 필요한 부분만 검증할 수 있음.

but, 지나치게 인터페이스를 작게 나누면 관리 비용이 증가할 수 있음.


1. REST API 설계에서 ISP 적용

실습 아이디어:

  • 단일 API 인터페이스를 설계할 때, 여러 종류의 클라이언트가 사용할 것을 염두에 두고 인터페이스를 분리합니다.

실습 방법:

  1. 요구사항: 간단한 도서 관리 시스템을 만들어 봅니다.

    • 관리자는 책을 추가/수정/삭제.
    • 일반 사용자는 책 목록 조회 및 검색만 가능.
  2. ISP 위반 코드 작성:

    • BookService에 모든 메서드 포함.
    public interface BookService {
        void addBook(Book book);
        void updateBook(Book book);
        void deleteBook(Long bookId);
        List<Book> getAllBooks();
        Book findBookById(Long bookId);
    }
  3. ISP 준수 코드 작성:

    • 인터페이스를 역할별로 분리.
    public interface BookManagementService {
        void addBook(Book book);
        void updateBook(Book book);
        void deleteBook(Long bookId);
    }
    
    public interface BookQueryService {
        List<Book> getAllBooks();
        Book findBookById(Long bookId);
    }
  4. 실제 구현:

    • BookManagementService는 관리자를 위한 서비스에 구현.
    • BookQueryService는 일반 사용자 API에 구현.

2. Spring Service와 Repository에서 역할 분리

실습 아이디어:

Spring에서 Service와 Repository 계층에 대해 인터페이스 분리 원칙을 적용해 봅니다.

실습 방법:

  1. 요구사항: 사용자 관리 시스템 구현.

    • 관리자는 사용자 등록, 수정, 삭제 가능.
    • 일반 사용자는 본인 정보만 조회.
  2. ISP 준수 코드:

    public interface UserManagementService {
        void createUser(User user);
        void updateUser(User user);
        void deleteUser(Long userId);
    }
    
    public interface UserQueryService {
        User findUserById(Long userId);
        List<User> findAllUsers();
    }
  3. 실제 구현:

    • UserManagementServiceImpl과 UserQueryServiceImpl 클래스를 각각 생성하여 구현.
    • 테스트 코드에서 관리 작업과 조회 작업이 독립적으로 동작하는지 검증.

3. Spring Security와 ISP 연계

실습 아이디어:

  • Spring Security에서 인증(authentication)과 권한(authorization)을 분리하여 인터페이스 설계를 연습합니다.

실습 방법:

  1. 요구사항: 인증 및 권한 관리 시스템 구현.

    • 사용자는 로그인 가능.
    • 관리자는 새로운 사용자 권한을 추가하거나 변경 가능.
  2. ISP 준수 설계:

    public interface AuthenticationService {
        User authenticate(String username, String password);
    }
    
    public interface AuthorizationService {
        void assignRole(Long userId, String role);
        List<String> getRoles(Long userId);
    }
  3. 실제 구현:

    • Spring Security의 UserDetailsService를 활용하여 AuthenticationService 구현.
    • AuthorizationService를 별도로 작성하여 권한 관리.
  4. 추가 연습:

    • Role-Based Access Control (RBAC) 기능 구현.

4. 결제 모듈 분리 연습

실습 아이디어:

  • 다양한 결제 수단(카드, 계좌이체, 포인트 등)을 사용하는 모듈을 설계하며 ISP를 적용해 봅니다.

실습 방법:

  1. 요구사항: 결제 시스템 설계.

    • 카드는 결제 및 취소 가능.
    • 포인트는 잔액 조회 및 사용 가능.
  2. ISP 준수 설계:

    public interface CardPaymentService {
        void payByCard(String cardNumber, double amount);
        void cancelPayment(String transactionId);
    }
    
    public interface PointPaymentService {
        double checkBalance(Long userId);
        void usePoints(Long userId, double amount);
    }
  3. 실제 구현:

    • CardPaymentService는 카드 결제 관련 API 호출 로직 작성.
    • PointPaymentService는 포인트 잔액 관리 로직 작성.
  4. 확장 연습:

    • 새로운 결제 수단(예: PayPal)을 추가하고 ISP에 따라 인터페이스 설계.

5. Mock 테스트 및 AOP 활용

실습 아이디어:

  • 분리된 인터페이스에 대해 Mock 객체를 활용한 단위 테스트를 작성하고, AOP로 공통 로직을 분리해 봅니다.

실습 방법:

  1. 요구사항: 모든 서비스 호출에 대해 로깅(Log) 기능 추가.

  2. ISP 준수 코드:

    • 역할별 인터페이스를 분리하고, 각각에 대해 테스트를 작성.
  3. AOP 적용:

    • 공통 로깅 로직을 @Aspect로 작성하여 분리된 인터페이스의 구현체에 적용.
  4. 추가 실습:

    • Mock 객체를 활용해 ISP 준수 여부를 테스트.

실습 플랫폼 및 학습 도구

  • 프로젝트 플랫폼: GitHub에 작은 프로젝트 생성 후 업로드.
  • 테스트 도구: JUnit, Mockito 사용.
  • 빌드 도구: Gradle 또는 Maven.
  • Spring Boot 활용: Service/Repository 계층 설계와 테스트.

결론

위 실습을 통해 인터페이스 분리 원칙을 적용한 유연한 설계를 경험할 수 있습니다. 실무에서도 자주 발생하는 설계 문제를 해결하면서 코드 품질을 향상시키는 연습이 됩니다. 작은 기능부터 시작해 점차 복잡한 시스템으로 확장해보세요!

0개의 댓글