A Philosophy of Software Design — John Ousterhout 4
APoSD Ch.6 — 범용 모듈이 더 깊다 (General-Purpose Modules are Deeper)
핵심 요약
범용(general-purpose) 접근은 특수목적(special-purpose) 접근보다 더 단순하고 깊은 인터페이스를 만든다.
주요 원칙:
범용 솔루션이 실제로는 사용되지 않는 기능을 포함할 수 있다는 두려움이 있지만, 실제로는 인터페이스를 단순화한다
특수목적 접근은 점진적 개발과 일치하는 것처럼 보이지만, 메서드 수를 늘려 인지 부하를 증가시킨다
범용 인터페이스는 적은 메서드로 더 많은 상황을 커버하여 인지 부하를 줄인다
핵심 통찰:
메서드 수를 줄이면서도 전체 기능을 유지하는 것이 더 범용적이다. 단, 메서드 파라미터 수가 늘어나서는 안 된다.
바이브 코딩 판단 포인트
AI가 코드를 생성할 때 흔히 저지르는 실수:
"지금 필요한 것만" 구현하려다 특수목적 메서드를 계속 추가
요구사항이 조금만 바뀌어도 새로운 메서드를 만듦
비슷한 기능의 메서드가 5개, 10개로 늘어남
AI 생성 코드를 심판할 때 보는 것:
인터페이스(public API)의 메서드 개수
각 메서드가 단 하나의 특수한 경우만 다루는가?
비슷한 메서드들을 하나로 통합할 수 있는가?
파라미터를 추가해서 여러 케이스를 커버할 수 있는가?
판단 기준:
❌ 메서드 15개로 15가지 경우를 커버
✅ 메서드 3개로 15가지 경우를 커버
판단 질문
AI가 생성한 코드를 리뷰할 때 스스로에게 묻기:
1. 가장 단순한 인터페이스는?
What is the simplest interface that will cover all my current needs?
API의 메서드 수를 줄이면서도 전체 기능을 유지할 수 있는가?
메서드를 줄이는 과정에서 파라미터 수가 과도하게 늘어나지는 않는가?
2. 이 메서드는 몇 가지 상황에서 쓰일까?
In how many situations will this method be used?
여러 특수목적 메서드를 하나의 범용 메서드로 대체할 수 있는가?
지금 당장은 한 가지 용도지만, 약간의 일반화로 다른 용도도 커버할 수 있는가?
3. 현재 필요에 쓰기 쉬운가?
Is this API easy to use for my current needs?
이 클래스를 현재 목적으로 사용하려고 많은 추가 코드를 작성해야 한다면, 올바른 기능이 아닐 수 있다
범용성을 추구하다가 사용성을 해치면 안 된다
Bad vs Good 예시
예시 1: 검색 API의 과잉 특수화
❌ Bad: AI가 요구사항마다 메서드 추가
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 초기 요구사항: 이메일로 찾기
Optional<User> findByEmail(String email);
// 새 요구사항: 이름으로 찾기
List<User> findByName(String name);
// 또 새 요구사항: 전화번호로 찾기
Optional<User> findByPhone(String phone);
// 또또 새 요구사항: 부서로 찾기
List<User> findByDepartment(String department);
// 또또또: 이름과 부서로 찾기
List<User> findByNameAndDepartment(String name, String department);
// 또또또또: 이메일 도메인으로 찾기
List<User> findByEmailDomain(String domain);
// ... 계속 늘어남 (메서드 폭발)
}
문제:
메서드가 계속 늘어남 (6개, 10개, 20개...)
각 메서드는 딱 하나의 케이스만 처리
새로운 조합이 필요하면 또 메서드 추가
인터페이스가 얕아짐 (shallow interface)
✅ Good: 범용 검색 메서드
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 범용 검색 메서드 (Specification 패턴 활용)
List<User> findAll(Specification<User> spec);
// 또는 Query DSL 스타일
List<User> search(UserSearchCriteria criteria);
}
// 검색 조건 빌더 (필요시)
public class UserSearchCriteria {
private String email;
private String name;
private String phone;
private String department;
public Specification<User> toSpecification() {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (email != null) {
predicates.add(cb.equal(root.get("email"), email));
}
if (name != null) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
// ... 다른 조건들
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
장점:
메서드 1~2개로 무한한 조합 지원
새로운 검색 조건 추가 시 인터페이스 변경 없음
더 깊은 인터페이스 (deeper interface)
사용자는 필요한 조건만 조합
사용 예:
// 이메일로 찾기
userRepository.findAll(where(emailEquals("user@example.com")));
// 이름과 부서로 찾기
userRepository.findAll(where(nameContains("김")).and(departmentEquals("개발팀")));
// 복잡한 조합도 가능
UserSearchCriteria criteria = UserSearchCriteria.builder()
.name("김")
.department("개발팀")
.build();
userRepository.search(criteria);
예시 2: 파일 저장 API
❌ Bad: 각 파일 타입마다 전용 메서드
@Service
public class FileStorageService {
public String saveProfileImage(MultipartFile file) {
// 프로필 이미지 전용 로직
String path = "/profiles/" + UUID.randomUUID() + ".jpg";
// ... 저장 로직
return path;
}
public String saveProductImage(MultipartFile file) {
// 상품 이미지 전용 로직
String path = "/products/" + UUID.randomUUID() + ".jpg";
// ... 저장 로직
return path;
}
public String saveDocument(MultipartFile file) {
// 문서 전용 로직
String path = "/documents/" + UUID.randomUUID() + ".pdf";
// ... 저장 로직
return path;
}
public String saveThumbnail(MultipartFile file) {
// 썸네일 전용 로직
String path = "/thumbnails/" + UUID.randomUUID() + "_thumb.jpg";
// ... 저장 로직
return path;
}
// ... 파일 타입마다 계속 추가
}
문제:
파일 타입마다 메서드 추가 (6개, 10개...)
핵심 로직은 거의 동일한데 메서드만 늘어남
새로운 파일 타입 추가 시 또 메서드 추가
✅ Good: 범용 저장 메서드
@Service
public class FileStorageService {
public String save(MultipartFile file, FileCategory category) {
String basePath = category.getBasePath();
String extension = getExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
String fullPath = basePath + filename;
// 통합된 저장 로직
store(file, fullPath);
return fullPath;
}
// 필요하면 옵션 추가
public String save(MultipartFile file, FileCategory category, FileOptions options) {
// 썸네일 생성, 압축 등 옵션 처리
// ...
}
}
public enum FileCategory {
PROFILE("/profiles/"),
PRODUCT("/products/"),
DOCUMENT("/documents/"),
THUMBNAIL("/thumbnails/");
private final String basePath;
FileCategory(String basePath) {
this.basePath = basePath;
}
public String getBasePath() {
return basePath;
}
}
장점:
메서드 1~2개로 모든 파일 타입 지원
새 파일 타입 추가는 enum만 확장
공통 로직 중복 제거
사용 예:
// 프로필 이미지 저장
fileStorageService.save(file, FileCategory.PROFILE);
// 상품 이미지 저장 (같은 메서드)
fileStorageService.save(file, FileCategory.PRODUCT);
// 옵션이 필요한 경우
fileStorageService.save(file, FileCategory.THUMBNAIL,
FileOptions.builder().resize(200, 200).build());
예시 3: 알림 발송 API
❌ Bad: 알림 타입마다 전용 메서드
@Service
public class NotificationService {
public void sendWelcomeEmail(User user) {
// 환영 이메일 전용
emailSender.send(user.getEmail(), "환영합니다", welcomeTemplate);
}
public void sendPasswordResetEmail(User user, String token) {
// 비밀번호 재설정 이메일 전용
emailSender.send(user.getEmail(), "비밀번호 재설정", resetTemplate);
}
public void sendOrderConfirmationEmail(User user, Order order) {
// 주문 확인 이메일 전용
emailSender.send(user.getEmail(), "주문 완료", orderTemplate);
}
public void sendShippingNotification(User user, Order order) {
// 배송 알림 전용
emailSender.send(user.getEmail(), "배송 시작", shippingTemplate);
}
// ... 알림 타입마다 메서드 추가 (10개, 20개...)
}
✅ Good: 범용 알림 메서드
@Service
public class NotificationService {
public void send(NotificationType type, User recipient, Map<String, Object> data) {
Template template = templateRegistry.get(type);
String subject = template.renderSubject(data);
String body = template.renderBody(data);
emailSender.send(recipient.getEmail(), subject, body);
}
// 또는 더 객체지향적 접근
public void send(Notification notification) {
notification.send(emailSender);
}
}
public enum NotificationType {
WELCOME("welcome-template"),
PASSWORD_RESET("password-reset-template"),
ORDER_CONFIRMATION("order-confirmation-template"),
SHIPPING("shipping-notification-template");
private final String templateName;
NotificationType(String templateName) {
this.templateName = templateName;
}
}
사용 예:
// 환영 이메일
notificationService.send(NotificationType.WELCOME, user, Map.of("name", user.getName()));
// 주문 확인 이메일
notificationService.send(NotificationType.ORDER_CONFIRMATION, user,
Map.of("orderId", order.getId(), "amount", order.getAmount()));
판단 시나리오
시나리오 1: AI가 CRUD 메서드를 각각 생성
AI 제안:
public void createUser(UserDto dto) { ... }
public void createProduct(ProductDto dto) { ... }
public void createOrder(OrderDto dto) { ... }심판:
❌ 각 엔티티마다 create 메서드 만들면 메서드 폭발
✅ 제네릭 또는 Base 클래스 활용 검토
단, 각 엔티티의 생성 로직이 정말 다르다면 특수화도 OK
시나리오 2: AI가 조건부 조회 메서드를 여러 개 생성
AI 제안:
List<Order> findByStatus(OrderStatus status);
List<Order> findByDate(LocalDate date);
List<Order> findByStatusAndDate(OrderStatus status, LocalDate date);
심판:
❌ 조합이 늘어날수록 메서드 폭발
✅ 범용 검색 메서드 + 조건 빌더 패턴 제안
단, 가장 자주 쓰는 2~3개는 편의 메서드로 남겨도 OK
시나리오 3: AI가 비즈니스 규칙마다 별도 메서드 생성
AI 제안:
boolean canUserAccessResource(User user, Resource resource);
boolean canUserEditResource(User user, Resource resource);
boolean canUserDeleteResource(User user, Resource resource);
boolean canUserShareResource(User user, Resource resource);
심판:
❓ 각 권한 체크 로직이 완전히 다른가?
✅ 다르다면 특수화 OK (비즈니스 규칙은 각각 명확해야 함)
❌ 비슷한 패턴이면 checkPermission(user, resource, Action) 방식 검토
한 줄 판단 기준
메서드 3개가 각각 다른 1가지만 하느니, 메서드 1개가 3가지를 유연하게 하게 하라. 단, 파라미터 지옥은 피하라.
황금 비율:
메서드 5개 미만: 특수화 OK
메서드 5~10개: 범용화 검토 시작
메서드 10개 이상: 범용화 필수
예외:
비즈니스 도메인 메서드(각각 의미가 명확): 특수화 OK
기술적 유틸리티 메서드(비슷한 패턴): 범용화 필수
댓글
댓글이 없습니다.
