A Philosophy of Software Design — John Ousterhout 1
APoSD Ch.2 — 복잡도의 본질 (The Nature of Complexity)
핵심 요약
복잡도는 코드를 이해하고 수정하기 어렵게 만드는 모든 것이다. AI가 생성한 코드를 판단할 때는 "내가 아니라 다른 개발자가 읽을 때"의 관점이 핵심이다. 코드 줄 수가 많아도 인지 부하(Cognitive Load)가 낮으면 더 단순한 것이다. 복잡도는 한 번에 오지 않고 작은 결정들이 쌓여서 온다. AI가 "이 정도는 괜찮겠지"라며 추가한 의존성이나 간접성이 누적되면 시스템이 망가진다.
바이브 코딩 판단 포인트
AI는 종종 "일반적인 패턴"을 적용하려다 불필요한 복잡도를 만든다. 예를 들어:
Change Amplification 체크: AI가 추상화 레이어를 여러 개 만들어서, 간단한 변경에도 5개 파일을 고쳐야 한다면 과잉 설계다.
Cognitive Load 체크: 메서드 이름만 봐서는 뭘 하는지 모르고, 3개 클래스를 왔다 갔다 해야 이해되면 인지 부하가 높다.
Unknown Unknowns 체크: 새 필드 하나 추가하려는데 어디를 고쳐야 할지 바로 안 보이면, 설계가 불명확(obscure)하다는 신호다.
AI가 만든 코드에서 의존성(Dependencies)과 불명확성(Obscurity)을 찾아라. 의존성은 "이 코드만 보고는 이해할 수 없다"는 신호고, 불명확성은 "중요한 정보가 숨겨져 있다"는 신호다.
판단 질문
1. 이 코드 한 부분을 바꾸면 몇 군데를 더 고쳐야 하는가?
좋음: 변경이 1~2개 파일 내에서 완결된다. 의존성이 명시적이다.
나쁨: 인터페이스-구현체-DTO-Mapper-Service-Controller를 모두 수정해야 한다. Change Amplification이 심하다.
2. 이 메서드가 뭘 하는지 이해하려면 몇 개의 다른 파일을 열어봐야 하는가?
좋음: 메서드 시그니처와 바디만 봐도 80% 이해된다. 낮은 인지 부하.
나쁨: 3개 인터페이스와 2개 유틸 클래스를 찾아봐야 로직을 이해할 수 있다. 높은 인지 부하.
3. 새 필드를 추가하려면 어디를 고쳐야 하는지 바로 알 수 있는가?
좋음: 명확한 구조로 수정 지점이 자명하다. 낮은 obscurity.
나쁨: "이거 바꾸면 다른 곳도 바뀌어야 할 것 같은데 어디지?"라는 생각이 든다. Unknown Unknowns.
Bad vs Good 예시
Bad: AI가 "확장 가능하게" 만든 주문 조회
// OrderQueryService.java
@Service
public class OrderQueryService {
private final OrderRepository repository;
private final OrderDtoMapper mapper;
private final OrderEnricher enricher;
public OrderResponse findOrder(Long orderId) {
Order order = repository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
OrderDto dto = mapper.toDto(order);
EnrichedOrderDto enriched = enricher.enrich(dto);
return mapper.toResponse(enriched);
}
}
// OrderDtoMapper.java (별도 파일)
@Component
public class OrderDtoMapper {
public OrderDto toDto(Order order) {
return new OrderDto(order.getId(), order.getAmount());
}
public OrderResponse toResponse(EnrichedOrderDto dto) {
return new OrderResponse(dto.getId(), dto.getAmount(), dto.getUserName());
}
}
// OrderEnricher.java (별도 파일)
@Component
public class OrderEnricher {
private final UserService userService;
public EnrichedOrderDto enrich(OrderDto dto) {
String userName = userService.getUserName(dto.getUserId());
return new EnrichedOrderDto(dto.getId(), dto.getAmount(), userName);
}
}
문제점:
Change Amplification: 주문에 필드 하나 추가하면 Order, OrderDto, EnrichedOrderDto, Mapper, Enricher, Response 모두 수정.
Cognitive Load: 단순 조회인데 3개 클래스와 2개 DTO를 추적해야 이해됨.
Dependencies: OrderQueryService는 mapper와 enricher에 의존. 이들은 또 다른 것에 의존. 체인이 길다.
Good: 단순하고 명확한 주문 조회
// OrderService.java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final UserRepository userRepository;
public OrderResponse getOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId));
String userName = userRepository.findById(order.getUserId())
.map(User::getName)
.orElse("Unknown");
return new OrderResponse(
order.getId(),
order.getAmount(),
userName
);
}
}
// OrderResponse.java (간단한 record)
public record OrderResponse(Long id, BigDecimal amount, String userName) {}
개선점:
낮은 Change Amplification: 필드 추가 시 Order, OrderResponse, getOrder() 메서드만 수정.
낮은 Cognitive Load: 한 메서드 안에서 흐름이 명확. 다른 파일 안 열어봐도 됨.
명시적 Dependencies: 필요한 의존성(repository 2개)이 명확하고 직접적.
코드는 길지만: 줄 수는 비슷하거나 오히려 짧지만, 이해하는 데 필요한 지식은 훨씬 적다.
한 줄 판단 기준
AI가 만든 추상화 레이어를 보고 "이게 없으면 변경이 더 쉬울까?"를 물어라. 그렇다면 복잡도를 추가한 것이다.
댓글
댓글이 없습니다.
