1. 인터페이스 분리원칙
“클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.”
- 한 인터페이스가 너무 많은 기능을 제공하면 클라이언트(사용자) 입장에서 불필요한 기능에 의존할 가능성이 높아진다.
- 인터페이스를 작고 명확하게 나누어 특정 클라이언트에 필요한 기능만 제공해야 한다.
- 변경이 한 곳에서만 이루어지도록 하여 영향 범위를 최소화한다.
2. 예시
2-1 상황
쇼핑몰이나 전자 상거래 시스템에서 상품과 관련된 서비스를 개발한다고 가정한다. 관리자와 일반 사용자가 시스템에서 다른 역할과 기능을 수행한다.
2-2 요구사항
- 관리자(Admin): 상품을 추가, 수정, 삭제할 수 있다.
- 일반 사용자(User): 상품 목록을 조회하고 상세 정보를 확인할 수 있다.
- 두 사용자 역할을 분리된 인터페이스로 구현하여 불필요한 메서드 의존성을 제거한다.
2-3 안좋은 예시
public interface ProductService {
void addProduct(String productName);
void updateProduct(Long productId, String productName);
void deleteProduct(Long productId);
List<String> listProducts();
String getProductDetail(Long productId);
}
문제점
- 관리자와 일반 사용자가 동일한 인터페이스를 사용하기 때문에 필요 없는 메서드 구현 또는 사용이 발생한다.
- 일반 사용자는
addProduct
,updateProduct
,deleteProduct
메서드를 사용할 필요가 없지만, 인터페이스에 강제된다.
2-4 좋은 예시
(1) 역할에 따라 인터페이스 분리
public interface ProductAdminService {
void addProduct(String productName);
void updateProduct(Long productId, String productName);
void deleteProduct(Long productId);
}
public interface ProductUserService {
List<String> listProducts();
String getProductDetail(Long productId);
}
(2) 서비스 구현
@Service
public class ProductAdminServiceImpl implements ProductAdminService {
@Override
public void addProduct(String productName) {
System.out.println("상품 추가: " + productName);
}
@Override
public void updateProduct(Long productId, String productName) {
System.out.println("상품 수정 [ID: " + productId + "] -> " + productName);
}
@Override
public void deleteProduct(Long productId) {
System.out.println("상품 삭제 [ID: " + productId + "]");
}
}
@Service
public class ProductUserServiceImpl implements ProductUserService {
@Override
public List<String> listProducts() {
return List.of("상품1", "상품2", "상품3");
}
@Override
public String getProductDetail(Long productId) {
return "상품 상세 정보 [ID: " + productId + "]";
}
}
(3) 컨트롤러 분리
@RestController
@RequestMapping("/admin/products")
public class ProductAdminController {
private final ProductAdminService productAdminService;
public ProductAdminController(ProductAdminService productAdminService) {
this.productAdminService = productAdminService;
}
@PostMapping
public void addProduct(@RequestParam String productName) {
productAdminService.addProduct(productName);
}
@PutMapping("/{productId}")
public void updateProduct(@PathVariable Long productId, @RequestParam String productName) {
productAdminService.updateProduct(productId, productName);
}
@DeleteMapping("/{productId}")
public void deleteProduct(@PathVariable Long productId) {
productAdminService.deleteProduct(productId);
}
}
@RestController
@RequestMapping("/products")
public class ProductUserController {
private final ProductUserService productUserService;
public ProductUserController(ProductUserService productUserService) {
this.productUserService = productUserService;
}
@GetMapping
public List<String> listProducts() {
return productUserService.listProducts();
}
@GetMapping("/{productId}")
public String getProductDetail(@PathVariable Long productId) {
return productUserService.getProductDetail(productId);
}
}
3. ISP 적용의 장점
- 명확한 역할 분리:
- 관리자와 사용자가 각기 다른 서비스 인터페이스에 의존.
- 일반 사용자는 관리 기능에 전혀 의존하지 않음.
- 유지보수성 향상:
- 변경이 필요한 인터페이스만 수정 가능.
- 예를 들어, 관리 기능이 변경되어도 사용자 기능에는 영향이 없다.
- 확장성 증가:
- 새로운 역할(예: 리뷰 기능)이 추가되더라도 기존 코드를 수정하지 않고 새로운 인터페이스와 구현체를 추가하면 된다.
4. 결론
Spring Boot에서 ISP를 적용하면 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 설계할 수 있다. 이를 통해 명확한 역할 분리, 유지보수성 향상, 확장 가능성을 얻을 수 있으며, 대규모 프로젝트에서도 구조적인 복잡성을 효과적으로 관리할 수 있다.