본문 바로가기

개발공부/Java

패스트캠퍼스 환급챌린지 28일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기

반응형

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

Ch11. 객체지향 고급 프로그래밍 - 설계 원칙 적용하기

 

자바 객체 지향 설계의 심화: JDBC 결합도 문제와 SOLID 원칙 적용하기 

데이터 영속성 관리를 위해 JDBC를 사용하여 애플리케이션 데이터를 데이터베이스에 저장하는 방법을 알아보았습니다. 이제 한 단계 더 나아가, 이 과정에서 발생할 수 있는 설계상의 문제점을 진단하고, 이를 SOLID 설계 원칙을 적용하여 어떻게 개선할 수 있는지 함께 고민해보고자 합니다. 특히, ShoppingContext와 JDBC 관련 코드 간의 높은 결합도를 해결하는 과정에 집중하여, 더욱 유연하고 확장 가능한 설계를 만들어나가겠습니다.


1. JDBC 활용의 그림자: 높은 결합도 문제 

우리가 이전에 구현했던 ShoppingContext 클래스는 쇼핑몰 비즈니스 로직(상품 카탈로그 준비, 주문 처리 등)을 담당하면서 동시에 JDBC를 이용한 데이터베이스 영속성 처리 로직(JDBC 드라이버 로드, 연결, SQL 실행 등)까지 포함하고 있었습니다.

이러한 설계는 다음과 같은 부작용을 낳습니다.

  • 성격이 다른 두 가지 코드가 과하게 섞여버린 상태입니다.
  • 높은 결합도(High Coupling): ShoppingContextjava.sql 패키지의 여러 클래스(예: SQLException, ResultSet, Statement, PreparedStatement, DriverManager, Connection)에 직접적으로 의존하게 됩니다. 이는 쇼핑몰 비즈니스 로직과 데이터베이스 접근 로직이라는
  • 유지보수 및 변경의 어려움: 만약 데이터베이스 종류를 바꾸거나(예: SQLite에서 MySQL로), 영속성 처리 방식을 변경해야 한다면(예: JDBC에서 JPA로), ShoppingContext 클래스 전체를 수정해야 합니다. 이는 변경에 취약하고 고치기 어려운 코드가 됩니다.
  • 재사용성 저하: ShoppingContext는 JDBC에 강하게 묶여 있으므로, 데이터베이스가 필요 없는 다른 맥락에서 쇼핑 로직만 재사용하기가 어렵습니다.

결국, 이는 낮은 응집도(Low Cohesion)높은 결합도(High Coupling)로 이어지며, 좋지 않은 소프트웨어 설계의 전형적인 예시가 됩니다.

2. SOLID 원칙으로 문제 해결하기: 의존성 역전 원칙(DIP)의 힘 

이러한 결합도 문제를 해결하기 위한 가장 강력한 도구 중 하나가 바로 SOLID 설계 원칙의 마지막 원칙인 의존성 역전 원칙(Dependency Inversion Principle, DIP)입니다.

2.1. 의존성 역전 원칙(DIP)이란?

의존성 역전 원칙(DIP)은 객체 지향 설계에서 느슨하게 결합된(loosely coupled) 소프트웨어 모듈을 위한 특정 방법론입니다. 이 원칙의 핵심은 다음과 같습니다:

  • 고수준 모듈(Policy Layer)은 저수준 모듈(Mechanism Layer)에 의존해서는 안 됩니다. 둘 다 추상화(인터페이스)에 의존해야 합니다.
  • 추상화는 세부 사항(구현)에 의존해서는 안 됩니다. 세부 사항이 추상화에 의존해야 합니다.

이는 사용자 혹은 비즈니스 정책(Policy)적인 부분과 기술적 구현 방식(Mechanism)의 변경 주기나 원인이 다르기 때문에, 이들을 구분하기 위해 활용됩니다. 즉, 의존 관계를 의도적으로 반대로 함으로써, 특정 구현(예: JDBC)에 직접 의존하지 않고 추상화된 인터페이스에 의존하게 만듭니다.

2.2. DIP 적용 단계: ShoppingContext 분리하기

우리 쇼핑몰 애플리케이션에 DIP를 적용하여 ShoppingContext와 JDBC 간의 높은 결합도를 해결해 보겠습니다.

1단계: 정책(Policy)과 메커니즘(Mechanism) 구분하기

먼저, ShoppingContext의 기능 중에서 정책(Policy)적인 부분(쇼핑몰의 비즈니스 흐름, 주문, 할인 등)과 메커니즘(Mechanism)적인 부분(데이터 영속성 처리 방식, 즉 JDBC 관련 코드)을 명확히 구분해야 합니다. 기존 ShoppingContext는 쇼핑 맥락을 담는 컨테이너 기능과, 판매 종료 시점에 데이터를 보관하는 정책을 가지고 있었습니다. 만약 매 주문마다 데이터를 보관하는 정책으로 변경해야 한다면, 이 persist() 메소드는 가변적인 부분이 됩니다.

2단계: 추상화(인터페이스) 도입 및 정책 구현 클래스 분리

DIP의 핵심은 추상화에 의존하는 것입니다. 따라서, ShoppingContext의 핵심 기능을 정의하는 인터페이스 IShoppingContext를 도입하고, 이 인터페이스를 구현하는 구체적인 클래스들을 만듭니다.

// IShoppingContext.java
import java.util.List;
import java.util.Map;

public interface IShoppingContext { //
    Map<String, Product> prepareCatalog();
    List<Customer> getSimulatedCustomerList(int count);
    void init();
    Order order(Customer customer, Product product);
    void startDailyDiscount(String productId);
    void close();
    void persist(); // 데이터 영속성 처리 기능도 인터페이스에 포함
    // ... (필요에 따라 다른 메소드 추가)
}
  • IShoppingContext 인터페이스: 쇼핑몰 컨텍스트가 제공해야 하는 모든 비즈니스 기능을 추상화하여 정의합니다.
  • 구체적인 정책 구현 클래스: IShoppingContext 인터페이스를 구현하여, 각각 다른 영속성 정책을 가진 ShoppingContext 구현체를 만듭니다.
    • ShoppingContextOnDailyBasis: 하루 단위로 데이터를 저장하는 정책 (기존 persist() 로직 유지)
    • ShoppingContextWithEveryOrderUpdate: 매 주문마다 데이터를 저장하는 정책 (새로운 영속성 로직 구현)

이렇게 하면 ShoppingContext 인터페이스를 사용하는 클라이언트(예: main 메소드)는 특정 구현체에 의존하지 않고, 인터페이스에만 의존하게 됩니다.

3단계: ISP(Interface Segregation Principle) 적용 고려

IShoppingContext 인터페이스를 만들었지만, 만약 이 인터페이스가 너무 많은 메소드를 포함하고 있다면 문제가 될 수 있습니다. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 "어떤 코드도 자신이 사용하지 않는 메소드에 의존하도록 강요받아서는 안 된다"고 명시합니다. 즉, 거대한 인터페이스를 여러 개의 작은 클라이언트 특화 인터페이스로 분리해야 한다는 것이죠.

현재 IShoppingContext는 모든 비즈니스 로직과 영속성 로직을 담고 있어, 이를 구현하는 모든 클래스가 모든 메소드를 구현해야 합니다. 만약 어떤 ShoppingContext 구현체가 persist() 기능을 필요로 하지 않는다면, 불필요한 메소드를 구현해야 하는 부담이 생깁니다.

이를 해결하기 위해 IShoppingContext를 더 작고 응집도 높은 여러 인터페이스로 분리할 수 있습니다 (예: ICatalogProvider, IOrderProcessor, IPersistenceManager 등). 이렇게 하면 각 구현 클래스는 자신이 필요한 인터페이스만 구현하면 됩니다. 이는 SRP(단일 책임 원칙)와도 유사하지만, 관계(Relation) 관점에서 의존성을 줄이는 데 초점을 맞춥니다.

4단계: OCP(Open-Closed Principle) 적용을 위한 추상 클래스 도입

ISP를 통해 인터페이스를 분리하는 것도 중요하지만, 공통적인 구현 로직이 있다면 이를 추상 클래스에 두어 개방-폐쇄 원칙(Open-Closed Principle, OCP)을 따르는 것이 좋습니다. OCP는 "소프트웨어 엔티티는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다"고 말합니다.IShoppingContext 인터페이스와 그 구현체들 사이에 AbstractShoppingContext라는 추상 클래스를 도입하여, 모든 ShoppingContext 구현체가 공통적으로 가지는 로직(예: prepareCatalog(), getSimulatedCustomerList(), init(), order(), startDailyDiscount(), close() 등)을 이곳에 구현합니다. 그리고 persist()afterOrderProcessing()와 같이 정책에 따라 달라지는 부분만 추상 메소드로 남겨두거나 하위 클래스에서 오버라이딩하도록 합니다.

// AbstractShoppingContext.java (추상 클래스)
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class AbstractShoppingContext implements IShoppingContext { //
    protected Map<String, Product> productCatalog = new HashMap<>();
    protected List<Customer> simulatedCustomers;
    protected Map<Integer, Order> orderMap = new HashMap<>(); // 주문 저장 맵
    protected int orderNumber = 0;

    @Override
    public Map<String, Product> prepareCatalog() { // 공통 구현
        // ... 상품 카탈로그 준비 로직 ...
        productCatalog.put("book1", ProductFactory.newBook("켄트 벡의 Tidy First?", 19800));
        productCatalog.put("food1", ProductFactory.newFood("맛있는 떡볶이", 6000));
        return productCatalog;
    }

    @Override
    public List<Customer> getSimulatedCustomerList(int count) { // 공통 구현
        // ... 고객 리스트 생성 로직 ...
        return null; // 실제 구현 생략
    }

    @Override
    public void init() { // 공통 구현
        System.out.println("판매 개시!");
    }

    @Override
    public Order order(Customer customer, Product product) { // 공통 주문 로직
        Order newOrder = new Order(customer, product);
        orderMap.put(++orderNumber, newOrder);
        afterOrderProcessing(newOrder); // 주문 후 처리 확장점
        return newOrder;
    }

    @Override
    public void startDailyDiscount(String productId) { // 공통 할인 로직
        // ... 할인 시작 로직 ...
    }

    @Override
    public void close() { // 공통 종료 로직
        // ... 마일리지 정산 등 ...
        persist(); // persist() 호출 (하위 클래스에서 구현된 persist()가 호출됨)
    }

    // 정책에 따라 달라지는 부분 (하위 클래스에서 구현하거나 오버라이딩)
    public abstract void persist(); // 데이터 영속화 처리
    protected void afterOrderProcessing(Order order) { // 주문 후 추가 처리 확장점
        // 기본적으로 아무것도 하지 않음. 하위 클래스에서 필요시 오버라이딩
    }
}

이제 구체적인 구현 클래스들은 AbstractShoppingContext를 상속받아 공통 로직은 재사용하고, 자신만의 persist()afterOrderProcessing() 메소드만 구현하면 됩니다. 예를 들어,

ShoppingContextOnDailyBasisclose() 시점에 persist()를 통해 하루 단위로 데이터를 저장하고, ShoppingContextWithEveryOrderUpdateorder() 메소드에서 호출되는 afterOrderProcessing()을 오버라이딩하여 매 주문마다 데이터를 DB에 반영할 수 있습니다.

5. 설계 원칙을 통한 코드의 진화 

오늘 우리는 자바 애플리케이션에서 JDBC를 사용할 때 발생할 수 있는 높은 결합도 문제를 진단하고, 이를 SOLID 설계 원칙의 DIP(의존성 역전 원칙)와 OCP(개방-폐쇄 원칙), 그리고 ISP(인터페이스 분리 원칙)를 적용하여 어떻게 해결할 수 있는지 알아보았습니다.

이러한 설계 원칙들은 단순히 추상적인 개념이 아닙니다. 이들은 실제 개발 현장에서 마주하는 복잡성을 줄이고, 코드의 유연성, 확장성, 그리고 무엇보다 유지보수성을 획기적으로 향상시키는 실질적인 지혜입니다. ShoppingContext의 예시처럼, 정책과 메커니즘을 분리하고, 추상화에 의존하며, 공통 로직을 추상 클래스에 두어 확장 지점을 마련하는 과정은 처음에는 복잡하게 느껴질 수 있습니다. 하지만 이는 마치 집을 지을 때 튼튼한 골조와 유연한 내부 설계를 확보하는 것과 같습니다.

오늘의 실습

오늘의 느낀점 

설계원칙이나 패턴은 사실 아직 잘 모르겠다. 하지만 단단하고 오래가는 프로그램을 위해서는 꼭 다시 한번 공부하고 기억해 내야겠다.

 

 

#패스트캠퍼스 #환급챌린지 #패스트캠퍼스후기 #습관형성 #직장인자기계발 #오공완

https://fastcampus.info/4n8ztzq

 

(~6/20) 50일의 기적 AI 환급반💫 | 패스트캠퍼스

초간단 미션! 하루 20분 공부하고 수강료 전액 환급에 AI 스킬 장착까지!

fastcampus.co.kr

 

반응형