본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
Ch 9. 디자인 패턴과 객체지향 설계 원칙
잘 만들어진 소프트웨어는 단순히 코드를 나열하는 것을 넘어, 체계적인 설계 원칙과 재사용 가능한 해결책을 바탕으로 합니다. 이번 글에서는 디자인 패턴, SOLID 설계 원칙, 그리고 프레임워크의 개념을 통해 유연하고 견고한 자바 애플리케이션을 구축하는 지혜를 나누어 드리고자 합니다.
1. 디자인 패턴: 검증된 설계 노하우
소프트웨어 공학에서 디자인 패턴(Design Pattern)은 컴퓨터 프로그램의 특정 측면(기능)을 코드를 어떻게 작성할지에 대한 관점에서 상대적으로 작고 잘 정의된 방식으로 기술합니다. 쉽게 말해, 소프트웨어 개발 과정에서 반복적으로 발생하는 문제들에 대한 모범적인 해결책을 정형화한 것입니다. 이는 'GoF(Gang of Four)'라 불리는 네 명의 저자가 1994년 출간한 『Design Patterns: Elements of Reusable Object-Oriented Software』라는 책을 통해 널리 알려졌습니다.
디자인 패턴은 크게 세 가지 유형으로 나눌 수 있습니다.
1.1. 생성 패턴 (Creational Patterns)
객체 생성 메커니즘을 다루며, 특정 상황에 적합한 방식으로 객체를 생성하려 노력하는 디자인 패턴입니다.
- 팩토리 메서드 패턴(Factory Method Pattern): 객체를 생성하는 인터페이스를 정의하되, 어떤 클래스의 인스턴스를 생성할지는 서브클래스에서 결정하도록 합니다. 이를 통해 객체 생성 로직을 캡슐화하고 유연성을 확보할 수 있습니다.
- 싱글톤 패턴(Singleton Pattern): 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 그 인스턴스에 대한 전역적인 접근 지점을 제공합니다. 스프링 프레임워크의 ApplicationContext나 FactoryBean 등이 이 패턴의 대표적인 적용 사례입니다.
- 그 외: 추상 팩토리 패턴(Abstract Factory Pattern), 빌더 패턴(Builder Pattern), 프로토타입 패턴(Prototype Pattern) 등이 있습니다.
ProductFactory 예시 (팩토리 메서드 패턴 유사):
이전에 만들었던 Product 클래스의 인스턴스를 생성할 때, 특정 규칙(예: 관리 번호 부여)을 적용하고 구체적인 상품 클래스(고정 가격, 일일 가격)를 선택하는 로직이 필요할 수 있습니다. 이럴 때 ProductFactory와 같은 클래스를 만들어 객체 생성 로직을 한 곳으로 모아 응집도를 높일 수 있습니다.
public class ProductFactory {
private static int bookCounter = 0; // 책 상품 관리 번호 카운터 [cite: 1061]
private static int foodCounter = 0; // 식품 상품 관리 번호 카운터 [cite: 1062]
// 새로운 책 상품 객체를 생성하고 고유 키를 부여 [cite: 1063]
public static Product newBook(String name, int price) {
Product product = new FixedPricedProduct(name, price); // 구체적인 FixedPricedProduct 클래스 선택 [cite: 1067]
// product.setKey("book" + ++bookCounter); // Key(관리 번호) 부여 규칙 [cite: 1066] (가정: Product에 setKey 메소드 추가)
return product;
}
// 새로운 식품 상품 객체를 생성하고 고유 키를 부여 [cite: 1071]
public static Product newFood(String name, int price) {
Product product = new DailyPricedProduct(name, price, false); // 구체적인 DailyPricedProduct 클래스 선택
// product.setKey("food" + ++foodCounter); // Key(관리 번호) 부여 규칙 [cite: 1066] (가정: Product에 setKey 메소드 추가)
return product;
}
}
주의: Product 클래스에 setKey() 메소드가 없으므로 실제 작동을 위해서는 추가 구현이 필요합니다.
1.2. 구조 패턴 (Structural Patterns)
엔티티 간의 관계를 단순화하여 설계를 용이하게 하는 디자인 패턴입니다.
- 어댑터 패턴(Adapter Pattern), 컴포지트 패턴(Composite Pattern), 데코레이터 패턴(Decorator Pattern), 파사드 패턴(Facade Pattern), 프록시 패턴(Proxy Pattern) 등이 있습니다.
1.3. 행위 패턴 (Behavioral Patterns)
객체 간의 공통적인 통신 패턴을 식별하여 통신 수행의 유연성을 높이는 디자인 패턴입니다.
- 커맨드 패턴(Command Pattern), 이터레이터 패턴(Iterator Pattern), 옵저버 패턴(Observer Pattern), 전략 패턴(Strategy Pattern), 템플릿 메서드 패턴(Template Method Pattern), 비지터 패턴(Visitor Pattern) 등이 있습니다. 특히, 스프링 프레임워크의 JdbcTemplate이나 RestTemplate 등에서 템플릿 메서드 패턴을 흔히 찾아볼 수 있습니다
2. SOLID 설계 원칙: 유연하고 유지보수 가능한 코드의 기반
SOLID는 객체 지향 설계를 더 이해하기 쉽고, 유연하며, 유지보수 가능하게 만들고자 하는 다섯 가지 디자인 원칙의 약어입니다. 로버트 C. 마틴(Robert C. Martin)이 2000년 논문에서 처음 제시했습니다.
2.1. 단일 책임 원칙 (Single-responsibility Principle, SRP)
"모듈은 하나의, 그리고 오직 하나의 액터에 대해서만 책임져야 한다."는 프로그래밍 원칙입니다. 이는 클래스나 모듈이 변경될 이유가 오직 하나여야 한다는 의미와도 통합니다. 예를 들어, 주문을 처리하는 클래스가 주문 데이터 관리, 결제 처리, 알림 전송까지 모두 담당한다면, 이 클래스는 여러 책임을 가지게 되어 변경이 발생할 때마다 수정될 가능성이 높아집니다. 책임을 분리하여 각 클래스가 명확한 하나의 역할만 담당하게 해야 합니다.
2.2. 개방-폐쇄 원칙 (Open-closed Principle, OCP)
"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다."는 원칙입니다. 이는 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 한다는 의미입니다. 상속, 인터페이스, 추상 클래스 등을 활용하여 확장 지점(extension points)을 제공함으로써 이 원칙을 지킬 수 있습니다.
OCP 적용 예시 (Product 클래스):
이전에 다뤘던 Product 클래스의 getPrice() 메소드를 생각해봅시다. 상품 가격을 계산하는 방식이 고정 가격, 할인 가격 등 다양하게 존재합니다. 만약 Product 클래스 자체에서 모든 가격 계산 로직을 담당한다면, 새로운 가격 정책이 추가될 때마다 Product 클래스를 수정해야 합니다. 하지만 getPrice()를 추상 메소드로 정의하고 하위 클래스에서 각자의 가격 정책에 따라 구현한다면, 새로운 가격 정책이 생겨도 기존 Product 클래스를 수정할 필요 없이 새로운 하위 클래스만 추가하면 됩니다.
public abstract class Product { //
protected String name;
protected int price;
protected String key; // 관리 번호 (모든 상품이 동일 규칙으로 가짐) [cite: 1981]
public String getName() {
return this.name;
}
public String getKey() {
return this.key;
}
protected void setKey(String key) { //
this.key = key;
}
// 가격은 하위 클래스에서 정의 [cite: 1985]
// (예시: PriceVisitor 패턴을 적용하여 가격 계산 로직을 분리할 수도 있음)
public abstract int getPrice(); // [cite: 1984] (원본 PDF에서는 PriceVisitor를 매개변수로 받으나, 단순화를 위해 제거)
}
2.3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
"하위 타입은 그것의 상위 타입으로 대체될 수 있어야 한다."는 원칙입니다. 즉, 어떤 객체를 그 객체의 부모 타입으로 교체해도 프로그램의 정확성이 유지되어야 한다는 의미입니다. 이는 다형성을 올바르게 활용하는 데 필수적이며, 인터페이스 구현을 통해서도 달성할 수 있습니다. 자바 컬렉션 프레임워크(Java Collection Framework)의 List, Map과 같은 인터페이스와 이를 구현하는 ArrayList, HashMap 등의 구현체들이 LSP의 좋은 예시입니다.
2.4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
"어떤 코드도 자신이 사용하지 않는 메소드에 의존하도록 강요받아서는 안 된다."는 원칙입니다. 이는 거대한 단일 인터페이스보다는 클라이언트에 특화된 여러 개의 작은 인터페이스를 선호해야 한다는 의미입니다. SRP와 유사하지만, 관계 관점에서 모듈 간의 의존성을 줄이는 데 초점을 맞춥니다.
2.5. 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
객체 지향 설계에서 의존성 역전 원칙은 느슨하게 결합된 소프트웨어 모듈을 위한 특정 방법론입니다. 이는 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 것을 의미합니다. 또한, 추상화는 세부 사항에 의존해서는 안 되고, 세부 사항이 추상화에 의존해야 한다는 원칙입니다.
이는 사용자 혹은 비즈니스 정책(Policy)적인 부분과 기술적 구현 방식(Mechanism)의 변경 주기나 원인이 다르기 때문에, 이를 구분하기 위해 활용됩니다. 의존 관계를 의도적으로 반대로 함으로써 (예: 인터페이스를 중간에 두어), 특정 구현에 직접 의존하지 않고 추상화된 인터페이스에 의존하게 만듭니다.
3. IoC(Inversion of Control)와 프레임워크: 흐름의 주도권
제어의 역전(Inversion of Control, IoC)은 컴퓨터 프로그램의 커스텀 작성된 부분이 일반적인 프레임워크로부터 제어 흐름을 받는 디자인 원칙입니다. 다시 말해, 애플리케이션의 제어 흐름이 개발자가 직접 작성한 코드가 아닌, 특정 프레임워크에 의해 결정되고 관리되는 방식입니다.
소프트웨어 프레임워크(Software Framework)는 일반적인 기능을 제공하는 소프트웨어의 추상화입니다. 이는 추가적인 사용자 작성 코드를 통해 선택적으로 변경될 수 있으며, 이를 통해 애플리케이션 특화된 소프트웨어를 제공합니다.
IoC와 프레임워크의 관계:
프레임워크는 미리 정의된 구조와 제어 흐름을 제공하며, 개발자는 그 흐름 속에서 자신의 비즈니스 로직을 채워 넣습니다. 개발자가 객체를 직접 생성하고 관리하는 대신, 프레임워크가 객체의 생성, 생명 주기 관리, 의존성 주입 등을 담당하는 경우가 많습니다. 스프링(Spring) 프레임워크가 대표적인 IoC 컨테이너이며, 의존성 주입(Dependency Injection, DI)은 IoC를 구현하는 대표적인 방법 중 하나입니다.
인터페이스 기반 프로그래밍은 모듈화된 프로그래밍을 구현하기 위한 아키텍처 패턴으로, 특히 모듈 시스템이 없는 객체 지향 프로그래밍 언어에서 컴포넌트 수준에서 활용됩니다. 이는 IoC를 구현하는 데 중요한 역할을 합니다. 인터페이스를 통해 모듈 간의 결합도를 낮추고 유연성을 확보할 수 있기 때문입니다.
3.1. 첫 번째 프레임워크 구현 시도 (개념적 접근)
우리가 만든 쇼핑몰 시나리오를 고정적인 흐름과 개별 행위로 구분하여 프레임워크 개념을 적용해 볼 수 있습니다.
고정적 흐름 (ShoppingContext - 가상의 프레임워크):
// ShoppingContext.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ShoppingContext { // 프레임워크의 고정된 흐름을 담당하는 컨텍스트
private Map<String, Product> productCatalog;
private List<Customer> simulatedCustomers;
private Map<Integer, Order> orderMap;
private int orderNumber = 0;
public ShoppingContext() {
this.productCatalog = new HashMap<>();
this.simulatedCustomers = new ArrayList<>();
this.orderMap = new HashMap<>();
}
// 1. 상품 카탈로그 준비 (고정적 흐름) [cite: 716]
public Map<String, Product> prepareCatalog() {
productCatalog.put("book1", ProductFactory.newBook("켄트 벡의 Tidy First?", 19800));
productCatalog.put("food1", ProductFactory.newFood("맛있는 떡볶이", 6000));
productCatalog.put("food2", ProductFactory.newFood("영주 사과", 2000));
productCatalog.put("food3", ProductFactory.newFood("제주 당근 1kg", 10000));
System.out.println("프레임워크: 상품 카탈로그 준비 완료.");
return productCatalog;
}
// 2. 고객 방문 시뮬레이션 (고정적 흐름) [cite: 717]
public List<Customer> getSimulatedCustomerList(int count) {
// 실제 시뮬레이션 로직에 따라 고객 리스트를 생성
for (int i = 0; i < count; i++) {
simulatedCustomers.add(new Customer("시뮬레이션_고객" + (i + 1), "010-0000-000" + (i + 1), "sim_user" + (i + 1) + "@example.com"));
}
System.out.println("프레임워크: 고객 방문 시뮬레이션 리스트 준비 완료.");
return simulatedCustomers;
}
// 3. 판매 개시 (고정적 흐름) [cite: 718]
public void init() {
System.out.println("프레임워크: 판매 개시!");
}
// 4. 주문 처리 (고정적 흐름, 내부에서 실제 주문 행위 위임) [cite: 719, 720, 721]
public Order order(Customer customer, Product product) {
Order newOrder = new Order(customer, product);
orderMap.put(++orderNumber, newOrder);
System.out.println("프레임워크: " + newOrder.toString()); // 주문 결과 출력
return newOrder;
}
// 추가적인 프레임워크 기능
public void startDiscount(String productId) { // 5. 마감 할인 시작 [cite: 720]
Product product = productCatalog.get(productId);
if (product instanceof DailyPricedProduct) {
((DailyPricedProduct) product).setNearTheDeadline(true);
System.out.println("프레임워크: " + product.getName() + " 마감 할인이 시작되었습니다.");
} else {
System.out.println("프레임워크: " + product.getName() + "는 마감 할인 대상이 아닙니다.");
}
}
public Map<Integer, Order> getOrderMap() {
return orderMap;
}
}
메인 애플리케이션 (main 메소드) - 프레임워크 흐름에 커스텀 코드 삽입:
public class FrameworkExample {
public static void main(String[] args) {
// 0. 프레임워크 흐름에 따른 공유 맥락을 담는다. [cite: 1938]
ShoppingContext shoppingContext = new ShoppingContext();
// 1. 상품 카탈로그 준비 [cite: 1939]
Map<String, Product> catalog = shoppingContext.prepareCatalog();
// 2. 고객 방문 시뮬레이션 [cite: 1941]
List<Customer> customers = shoppingContext.getSimulatedCustomerList(4); // 4명의 고객 시뮬레이션 [cite: 1942]
// 3. 판매 개시 [cite: 1943]
shoppingContext.init();
// 4. 주문 (정상 가격) [cite: 1944]
shoppingContext.order(customers.get(0), catalog.get("book1")); // [cite: 1945]
shoppingContext.order(customers.get(1), catalog.get("food1"));
shoppingContext.order(customers.get(2), catalog.get("food2"));
shoppingContext.order(customers.get(3), catalog.get("food3"));
// 5. 마감 할인 시작 [cite: 720]
System.out.println("\n--- 프레임워크: 특정 품목 할인 시작 ---");
shoppingContext.startDiscount("food1"); // 떡볶이 할인 시작
// 6. 일부 품목 할인된 가격으로 주문 (할인 가격) [cite: 721]
System.out.println("\n--- 프레임워크: 할인된 가격으로 재주문 시도 ---");
// DailyPricedProduct는 getPrice() 내부에서 nearTheDeadline 값에 따라 할인 적용
shoppingContext.order(customers.get(0), catalog.get("food1")); // 할인된 떡볶이 재주문
System.out.println("\n최종 주문 내역:");
shoppingContext.getOrderMap().forEach((orderNum, order) -> System.out.println(orderNum + ". " + order));
}
}
이러한 구조는 고정적인 흐름을 담은 구조체(프레임워크)와 개별 행위를 구분하여 정의할 수 있게 합니다.
4. GRASP 설계 원칙: 객체 책임 할당의 지침
GRASP(General Responsibility Assignment Software Patterns)는 객체 설계 및 책임 할당에 대한 아홉 가지 기본 원칙들의 집합으로, Craig Larman이 1997년 저서에서 처음 발표했습니다. 이 원칙들은 객체 지향 시스템에서 객체에게 책임을 할당하는 방법을 안내합니다.
주요 GRASP 원칙은 다음과 같습니다.
- 정보 전문가(Information Expert): 정보를 가장 잘 아는 객체에게 책임을 할당합니다.
- 생성자(Creator): 객체를 생성하는 책임을 어떤 객체에 할당할지 결정합니다.
- 컨트롤러(Controller) / 순수 인공물(Pure Fabrication): 시스템 이벤트에 대한 책임을 담당합니다. ShoppingMall 클래스가 여러 Use Case의 진입점 역할을 하는 것이 컨트롤러 패턴의 한 예시가 될 수 있습니다.
- 간접(Indirection): 두 개체 간의 결합을 피하기 위해 중간 개체를 만듭니다 (예: MVC 패턴).
- 낮은 결합도(Low Coupling): 모듈 간의 의존성을 최소화합니다.
- 높은 응집도(High Cohesion): 모듈 내부의 요소들이 하나의 목적을 위해 밀접하게 관련되도록 합니다.
- 다형성(Polymorphism): 다양한 타입의 객체에 단일 인터페이스를 제공하여 유연성을 높입니다.
- 보호된 변경(Protected Variations): 시스템의 변경 사항으로 인한 파급 효과를 방지하기 위해 인터페이스를 사용하여 변경되는 부분을 캡슐화합니다.
마무리
- 디자인 패턴은 재사용 가능하고 검증된 설계 솔루션을 제공하여 개발 효율성을 높입니다.
- SOLID 원칙은 유연하고 이해하기 쉬우며 유지보수 가능한 객체 지향 코드를 작성하기 위한 지침을 제시합니다.
- IoC와 프레임워크는 애플리케이션의 제어 흐름을 역전시켜 개발자가 비즈니스 로직에 집중할 수 있도록 돕고, 시스템 전체의 구조와 재사용성을 향상시킵니다.
- GRASP 원칙은 객체에게 책임을 효과적으로 할당하여 설계의 품질을 높이는 실질적인 방법을 제공합니다.
오늘의실습
오늘의 느낀점
초반에 스프링 프레임워크 배울때 생각이 났다. 당시에 의존성 주입이며 뭐며 당체 뭔 소린지 하나도 알아들을 수가 없어서 스프링 프레임워크 관련 책만 얼마나 찾아봤던지... 이번 강의 챕터를 들으면서 그때 생각이 딱! 났다 ㅎㅎㅎ 디자인 패턴 관련해서도 엄청 공부하고 싶었는데, 지난 학기에 방통대 소프트웨어 공학 강의를 수강을 안해서 결국 금번학기 졸업이니 들을 일이 없게 되어 따로 찾아봐야 한다!
VSCODE 를 IDE 로 코드 생성하는게 너무 익숙하지 않아서 강의 따라갈때 좀 버벅이지만(아.. 벌써 이런 나이라니..ㅡㅜ) 그래도 재밌게 따라해보고 있다!
https://fastcampus.info/4n8ztzq
(~6/20) 50일의 기적 AI 환급반💫 | 패스트캠퍼스
초간단 미션! 하루 20분 공부하고 수강료 전액 환급에 AI 스킬 장착까지!
fastcampus.co.kr
'개발공부 > Java' 카테고리의 다른 글
패스트캠퍼스 환급챌린지 26일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (1) | 2025.07.26 |
---|---|
패스트캠퍼스 환급챌린지 25일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (2) | 2025.07.25 |
패스트캠퍼스 환급챌린지 23일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (3) | 2025.07.23 |
패스트캠퍼스 환급챌린지 22일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (1) | 2025.07.22 |
패스트캠퍼스 환급챌린지 21일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (2) | 2025.07.21 |