A Philosophy of Software Design — John Ousterhout 6
APoSD Ch.9 — 합칠까 나눌까 (Better Together Or Better Apart?)
핵심 요약
두 기능을 합칠지 분리할지 결정하는 기준:
목표: 시스템 전체 복잡도를 줄이고 모듈성을 향상
분리의 함정: 더 많은 인터페이스, 관리 코드 증가, 존재를 인지하기 어려움, 코드 중복
통합의 신호: 정보를 공유함, 함께 사용됨, 개념적으로 겹침, 하나를 이해하려면 다른 것을 봐야 함
통합해야 하는 경우:
더 단순한 인터페이스를 정의할 수 있을 때
자동화로 사용자가 기능을 인지하지 않아도 될 때
중복을 제거할 수 있을 때
분리해야 하는 경우:
범용(general-purpose)과 특수 목적(special-purpose) 코드
특수 목적 코드는 상위 레이어로 끌어올림
메서드 분할 원칙:
길이 자체는 분할 이유가 아님
시스템 전체가 더 단순해질 때만 분할
Deep module: 깔끔하고 단순한 추상화
바이브 코딩 판단 포인트
AI가 과도하게 분리하는 패턴
단일 용도 Validator 클래스
하나의 검증만 하는 별도 클래스 생성
3줄짜리 검증 로직을 클래스로 추출
지나치게 세분화된 Service 메서드
한 번만 호출되는 private 메서드 남발
5줄 메서드를 2줄씩 쪼개기
불필요한 Interface/구현체 분리
구현체가 하나뿐인 인터페이스
"확장성"을 위한 추상화 (YAGNI 위반)
Utility 클래스 남발
한 곳에서만 쓰는 유틸리티 메서드
도메인 로직을 억지로 static으로 추출
AI가 통합해야 하는데 분리하는 경우
함께 변경되는 코드
DTO 변환 로직이 여러 곳에 분산
같은 도메인 규칙이 중복 구현
정보를 공유하는 코드
같은 데이터를 다루는데 클래스가 3개
상태를 여러 객체에 나눠서 관리
하나 이해하려면 다른 것도 봐야 하는 코드
OrderProcessor, OrderValidator, OrderHelper가 긴밀히 협력
각자 일부 책임만 가져서 전체 흐름 파악 어려움
판단 질문
Q1. 이 분리가 인터페이스를 단순하게 만드는가?
❌ NO: 클래스는 늘었는데 호출 복잡도는 그대로
✅ YES: 여러 단계 호출을 하나로 통합
Q2. 이 코드는 다른 곳에서도 쓰이는가?
❌ NO (한 곳만): 억지로 분리하지 말 것
✅ YES (여러 곳): 공통 모듈로 추출 고려
Q3. 이 분리가 중복을 제거하는가?
❌ NO: 오히려 보일러플레이트 증가
✅ YES: 반복되던 코드가 한 곳으로
Q4. 범용 로직과 특수 로직이 섞여있는가?
❌ 섞임: 특수 로직을 상위 레이어로
✅ 분리됨: 적절한 계층화
Q5. 메서드 길이가 긴 것이 분할 이유인가?
❌ YES: 길이는 이유가 아님, 복잡도를 봐라
✅ NO: 더 단순한 추상화를 만들기 위함
Bad vs Good 예시
예시 1: 불필요한 Validator 분리 (BAD)
// AI가 생성한 코드 - 과도한 분리
@Component
public class OrderAmountValidator {
public void validate(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}
@Component
public class OrderItemValidator {
public void validate(Order order) {
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
}
}
@Component
public class OrderCustomerValidator {
public void validate(Order order) {
if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID required");
}
}
}
@Service
public class OrderService {
private final OrderAmountValidator amountValidator;
private final OrderItemValidator itemValidator;
private final OrderCustomerValidator customerValidator;
// 3개 validator를 각각 주입하고 호출
public void createOrder(Order order) {
amountValidator.validate(order);
itemValidator.validate(order);
customerValidator.validate(order);
// ... 실제 로직
}
}
문제점:
3개 클래스, 3개 주입, 3번 호출 → 인터페이스가 복잡해짐
각 Validator가 Order를 이해해야 함 (정보 공유)
하나를 이해하려면 다른 것도 봐야 함
한 번만 사용됨 (재사용성 없음)
// GOOD - 관련 검증을 한 곳에
@Service
public class OrderService {
public void createOrder(Order order) {
validateOrder(order);
// ... 실제 로직
}
private void validateOrder(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID required");
}
}
}
개선점:
1개 메서드로 통합 → 인터페이스 단순
검증 로직이 한 곳에 → 이해하기 쉬움
불필요한 추상화 제거
예시 2: 지나치게 세분화된 Service 메서드 (BAD)
// AI가 생성한 코드 - 과도한 메서드 분할
@Service
public class UserService {
public User registerUser(UserRegistrationRequest request) {
validateRequest(request);
User user = createUserEntity(request);
hashPassword(user);
saveUser(user);
sendWelcomeEmail(user);
logRegistration(user);
return user;
}
private void validateRequest(UserRegistrationRequest request) {
checkEmail(request.getEmail());
checkPassword(request.getPassword());
}
private void checkEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
private void checkPassword(String password) {
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password too short");
}
}
private User createUserEntity(UserRegistrationRequest request) {
User user = new User();
setUserEmail(user, request.getEmail());
setUserName(user, request.getName());
return user;
}
private void setUserEmail(User user, String email) {
user.setEmail(email);
}
private void setUserName(User user, String name) {
user.setName(name);
}
private void hashPassword(User user) {
String hashed = passwordEncoder.encode(user.getPassword());
user.setPassword(hashed);
}
private void saveUser(User user) {
userRepository.save(user);
}
private void sendWelcomeEmail(User user) {
emailService.send(user.getEmail(), "Welcome!");
}
private void logRegistration(User user) {
log.info("User registered: {}", user.getId());
}
}
문제점:
10개 메서드로 쪼개짐 → 전체 흐름 파악 어려움
setUserEmail, setUserName 같은 1줄짜리 메서드
각 메서드가 한 곳에서만 호출됨
길이 때문에 분할했지만 복잡도는 줄지 않음
// GOOD - 의미 있는 단위로만 분할
@Service
public class UserService {
public User registerUser(UserRegistrationRequest request) {
validateRequest(request);
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
userRepository.save(user);
emailService.send(user.getEmail(), "Welcome!");
log.info("User registered: {}", user.getId());
return user;
}
private void validateRequest(UserRegistrationRequest request) {
if (request.getEmail() == null || !request.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
if (request.getPassword() == null || request.getPassword().length() < 8) {
throw new IllegalArgumentException("Password too short");
}
}
}
개선점:
주요 단계만 메서드로 분할 (검증만 별도)
단순한 할당은 인라인으로
전체 흐름이 한눈에 보임
예시 3: 불필요한 Interface 분리 (BAD)
// AI가 생성한 코드 - "확장성"을 위한 추상화
public interface OrderCalculator {
BigDecimal calculateTotal(Order order);
}
@Component
public class DefaultOrderCalculator implements OrderCalculator {
@Override
public BigDecimal calculateTotal(Order order) {
return order.getItems().stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@Service
public class OrderService {
private final OrderCalculator orderCalculator;
public OrderService(OrderCalculator orderCalculator) {
this.orderCalculator = orderCalculator;
}
public void processOrder(Order order) {
BigDecimal total = orderCalculator.calculateTotal(order);
// ...
}
}
문제점:
구현체가 하나뿐 (다른 구현이 필요할 일 없음)
"나중을 위한" 추상화 (YAGNI)
인터페이스 추가로 파일 2개, 복잡도 증가
실제 이득 없음
// GOOD - 필요할 때만 추상화
@Service
public class OrderService {
public void processOrder(Order order) {
BigDecimal total = calculateTotal(order);
// ...
}
private BigDecimal calculateTotal(Order order) {
return order.getItems().stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
개선점:
단순한 계산은 private 메서드로
두 번째 구현이 실제로 필요해질 때 인터페이스 추출
파일 1개, 이해하기 쉬움
예시 4: 범용/특수 목적 분리 (GOOD)
// BAD - 특수 목적 코드가 하위 레이어에
@Component
public class DateUtils {
public LocalDate getCurrentKoreanDate() {
return LocalDate.now(ZoneId.of("Asia/Seoul"));
}
public LocalDate getTomorrowKoreanDate() {
return getCurrentKoreanDate().plusDays(1);
}
public LocalDate getNextBusinessDay() {
LocalDate date = getCurrentKoreanDate();
if (date.getDayOfWeek() == DayOfWeek.FRIDAY) {
return date.plusDays(3);
} else if (date.getDayOfWeek() == DayOfWeek.SATURDAY) {
return date.plusDays(2);
}
return date.plusDays(1);
}
}
문제점:
범용 유틸과 특수 비즈니스 로직 혼재
getNextBusinessDay는 도메인 규칙 (DateUtils에 어울리지 않음)
// GOOD - 범용은 하위, 특수는 상위
@Component
public class DateUtils {
// 범용 로직만
public LocalDate getCurrentDate(ZoneId zoneId) {
return LocalDate.now(zoneId);
}
public LocalDate addDays(LocalDate date, int days) {
return date.plusDays(days);
}
}
@Service
public class OrderService {
private final DateUtils dateUtils;
// 특수 목적 로직은 상위 레이어에
public LocalDate calculateDeliveryDate() {
LocalDate today = dateUtils.getCurrentDate(ZoneId.of("Asia/Seoul"));
// 비즈니스 규칙: 금요일 주문은 월요일 배송
if (today.getDayOfWeek() == DayOfWeek.FRIDAY) {
return dateUtils.addDays(today, 3);
} else if (today.getDayOfWeek() == DayOfWeek.SATURDAY) {
return dateUtils.addDays(today, 2);
}
return dateUtils.addDays(today, 1);
}
}
개선점:
DateUtils는 범용 날짜 연산만
비즈니스 규칙은 Service 레이어에
명확한 계층 분리
한 줄 판단 기준
합쳐야 할 신호
"이 클래스를 이해하려면 저 클래스도 봐야 하네" → 합쳐라
"이 메서드는 여기서만 호출되는데?" → 인라인으로
"같은 정보를 3곳에서 다루네" → 한 곳에 모아라
"인터페이스가 복잡해졌는데 내부는 단순해지지 않았네" → 통합 고려
나눠야 할 신호
"이 메서드 절반은 범용, 절반은 특수 목적이네" → 특수 부분을 상위로
"같은 코드가 3곳에 복사되어 있네" → 공통 모듈로 추출
"이 메서드 안에 2개의 명확히 다른 추상화가 있네" → 분리 고려
AI 코드 리뷰 체크리스트
✅ 이 분리가 전체 시스템을 단순하게 만드는가? (파일 수↑는 복잡도↑)
✅ 추출된 코드가 2곳 이상에서 사용되는가? (1곳만이면 의심)
✅ 인터페이스가 실제로 여러 구현체를 가질 것인가? (하나면 YAGNI)
✅ 메서드 분할이 길이 때문인가? (길이는 이유가 아님)
✅ 하위 레이어에 특수 목적 코드가 있는가? (상위로 올려라)
댓글
댓글이 없습니다.
