코드를 작성하다 보면 어느새 수백 줄의 거대한 메서드를 마주하게 되는 경험, 다들 한 번쯤 해보셨을 겁니다. 이런 '대형 메서드'는 마치 정리되지 않은 서랍장과 같아요. 😱 필요한 것을 찾기 위해 모든 내용물을 뒤져야 하고, 새로운 것을 추가하기도 어렵죠.
여러분이 일상에서 정리정돈을 하는 것처럼 코드도 정리가 필요합니다.
- 큰 메서드는 작은 메서드들로 분리하여 각각의 역할을 명확히 합니다
- 각 메서드는 하나의 일만 수행하도록 만들어 코드의 가독성을 높입니다
- 이러한 과정을 '메서드 분리 리팩토링'이라고 합니다
왜 필요한가?
대형 메서드가 일으키는 문제들은 다음과 같습니다:
- 이해하기 어려움 📚: 한 메서드에 너무 많은 로직이 섞여 있어 코드의 흐름을 파악하기 힘듭니다.
- 유지보수 어려움 🔧: 작은 변경 사항도 전체 메서드에 영향을 줄 수 있어 수정이 부담스럽습니다.
- 테스트 복잡성 🧪: 큰 메서드는 테스트 케이스 작성이 어렵고, 테스트 범위가 넓어 버그 발견이 어렵습니다.
- 코드 재사용성 저하 ♻️: 기능이 하나의 큰 메서드에 묶여 있어 다른 곳에서 재사용하기 어렵습니다.
- 단일 책임 원칙 위반 ⚠️: 하나의 메서드가 여러 책임을 가지게 되어 객체지향 설계 원칙에 위배됩니다.
기본 원리
대형 메서드 분리의 핵심 원리를 알아볼까요?
단일 책임 원칙 (SRP)
메서드는 단 하나의 책임만 가져야 합니다. 여러 일을 하는 메서드는 각각의 책임에 맞게 분리해야 합니다.
// 책임이 혼합된 메서드
public void processOrder(Order order) {
// 주문 검증, 가격 계산, 재고 업데이트, 결제 처리, 알림 발송 등
// 모든 로직이 한 메서드에 혼합되어 있음
}
응집도와 결합도
좋은 메서드는 높은 응집도(cohesion)와 낮은 결합도(coupling)를 가집니다. 즉, 관련된 코드는 함께 있고(응집도), 다른 부분과의 의존성은 최소화(결합도)해야 합니다.
대형 메서드 분리 기법
1. 메서드 추출 (Extract Method) 🔍
가장 기본적이고 강력한 리팩토링 기법입니다. 함께 동작하는 코드 조각을 찾아 별도의 메서드로 분리합니다.
// 리팩토링 전: 하나의 큰 메서드
public double calculatePrice(Order order) {
// 기본 가격 계산
double basePrice = 0;
for (Item item : order.getItems()) {
basePrice += item.getQuantity() * item.getUnitPrice();
}
// 할인 계산
double discount = 0;
if (order.getCustomer().isVIP()) {
discount = basePrice * 0.1;
} else if (order.getItems().size() > 5) {
discount = basePrice * 0.05;
}
// 배송비 계산
double shippingCost = 0;
if (basePrice > 50000) {
shippingCost = 0;
} else {
shippingCost = 2500;
}
return basePrice - discount + shippingCost;
}
// 리팩토링 후: 여러 작은 메서드로 분리
public double calculatePrice(Order order) {
double basePrice = calculateBasePrice(order);
double discount = calculateDiscount(order, basePrice);
double shippingCost = calculateShippingCost(basePrice);
return basePrice - discount + shippingCost;
}
private double calculateBasePrice(Order order) {
double basePrice = 0;
for (Item item : order.getItems()) {
basePrice += item.getQuantity() * item.getUnitPrice();
}
return basePrice;
}
private double calculateDiscount(Order order, double basePrice) {
if (order.getCustomer().isVIP()) {
return basePrice * 0.1;
} else if (order.getItems().size() > 5) {
return basePrice * 0.05;
}
return 0;
}
private double calculateShippingCost(double basePrice) {
return basePrice > 50000 ? 0 : 2500;
}
2. 임시 변수를 메서드로 교체 (Replace Temp with Query) 🔄
계산 결과를 임시 변수에 저장하는 대신, 해당 계산을 수행하는 메서드를 만들어 호출합니다.
// 리팩토링 전: 임시 변수 사용
public double calculateTotal(Order order) {
double basePrice = order.getQuantity() * order.getUnitPrice();
double discountFactor = 0.98;
if (basePrice > 1000) {
discountFactor = 0.95;
}
return basePrice * discountFactor;
}
// 리팩토링 후: 임시 변수를 메서드로 교체
public double calculateTotal(Order order) {
return getBasePrice(order) * getDiscountFactor(order);
}
private double getBasePrice(Order order) {
return order.getQuantity() * order.getUnitPrice();
}
private double getDiscountFactor(Order order) {
return getBasePrice(order) > 1000 ? 0.95 : 0.98;
}
3. 조건문 분해 (Decompose Conditional) 🧩
복잡한 조건문을 이해하기 쉬운 메서드로 분리합니다.
// 리팩토링 전: 복잡한 조건문
public double calculateCharge(Date date, int quantity) {
double charge;
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}
return charge;
}
// 리팩토링 후: 조건문 분해
public double calculateCharge(Date date, int quantity) {
if (isWinter(date)) {
return calculateWinterCharge(quantity);
} else {
return calculateSummerCharge(quantity);
}
}
private boolean isWinter(Date date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
private double calculateWinterCharge(int quantity) {
return quantity * winterRate + winterServiceCharge;
}
private double calculateSummerCharge(int quantity) {
return quantity * summerRate;
}
4. 조건문을 다형성으로 교체 (Replace Conditional with Polymorphism) 🔀
타입에 따라 다른 동작을 하는 조건문을 클래스 계층 구조와 다형성을 이용해 교체합니다.
// 리팩토링 전: 조건문 사용
public class Employee {
public static final int ENGINEER = 0;
public static final int MANAGER = 1;
public static final int SALESPERSON = 2;
private int type;
public double calculatePay() {
switch (type) {
case ENGINEER:
return calculateEngineerPay();
case MANAGER:
return calculateManagerPay();
case SALESPERSON:
return calculateSalespersonPay();
default:
throw new RuntimeException("Unknown employee type");
}
}
private double calculateEngineerPay() { /* ... */ }
private double calculateManagerPay() { /* ... */ }
private double calculateSalespersonPay() { /* ... */ }
}
// 리팩토링 후: 다형성 사용
public abstract class Employee {
public abstract double calculatePay();
}
public class Engineer extends Employee {
@Override
public double calculatePay() {
// 엔지니어 급여 계산 로직
}
}
public class Manager extends Employee {
@Override
public double calculatePay() {
// 매니저 급여 계산 로직
}
}
public class Salesperson extends Employee {
@Override
public double calculatePay() {
// 영업사원 급여 계산 로직
}
}
실제 예제
실제 프로젝트에서 대형 메서드를 분리하는 과정을 살펴보겠습니다.
사용자 인증 서비스 리팩토링
다음은 사용자 인증을 처리하는 대형 메서드입니다:
// 리팩토링 전: 모든 인증 로직이 하나의 메서드에 있음
public class AuthenticationService {
public AuthResult authenticate(String username, String password, String ipAddress) {
// 1. 입력 유효성 검사
if (username == null || username.trim().isEmpty()) {
logger.warn("인증 실패: 사용자명이 비어 있음");
return new AuthResult(false, "사용자명이 필요합니다.");
}
if (password == null || password.isEmpty()) {
logger.warn("인증 실패: 비밀번호가 비어 있음");
return new AuthResult(false, "비밀번호가 필요합니다.");
}
// 2. 사용자 조회
User user = userRepository.findByUsername(username);
if (user == null) {
logger.warn("인증 실패: 사용자 {} 없음", username);
return new AuthResult(false, "사용자명 또는 비밀번호가 잘못되었습니다.");
}
// 3. 계정 상태 확인
if (!user.isActive()) {
logger.warn("인증 실패: 사용자 {} 계정 비활성화됨", username);
return new AuthResult(false, "계정이 비활성화되었습니다.");
}
// 4. 계정 잠금 확인
if (user.isLocked()) {
if (user.getLockExpirationTime().after(new Date())) {
logger.warn("인증 실패: 사용자 {} 계정 잠김", username);
return new AuthResult(false, "계정이 잠겼습니다. 나중에 다시 시도하세요.");
} else {
user.setLocked(false);
user.setLoginAttempts(0);
userRepository.save(user);
}
}
// 5. 비밀번호 확인
if (!passwordEncoder.matches(password, user.getPassword())) {
logger.warn("인증 실패: 사용자 {} 잘못된 비밀번호", username);
// 로그인 시도 증가 및 계정 잠금 처리
user.setLoginAttempts(user.getLoginAttempts() + 1);
if (user.getLoginAttempts() >= 5) {
user.setLocked(true);
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MINUTE, 30);
user.setLockExpirationTime(cal.getTime());
logger.warn("사용자 {} 계정 잠김", username);
}
userRepository.save(user);
return new AuthResult(false, "사용자명 또는 비밀번호가 잘못되었습니다.");
}
// 6. 로그인 성공 처리
user.setLoginAttempts(0);
user.setLastLoginDate(new Date());
user.setLastLoginIp(ipAddress);
userRepository.save(user);
// 7. 로그인 이벤트 기록
LoginEvent event = new LoginEvent();
event.setUsername(username);
event.setIpAddress(ipAddress);
event.setTimestamp(new Date());
event.setSuccess(true);
eventRepository.save(event);
// 8. 보안 검사 - 의심스러운 로그인 확인
if (!ipAddress.equals(user.getLastLoginIp())) {
// 위치 변경 감지, 이메일 알림 발송
notificationService.sendSecurityAlert(user.getEmail(), ipAddress, new Date());
logger.info("사용자 {} 위치 변경 감지, 보안 알림 발송됨", username);
}
// 9. JWT 토큰 생성
String token = jwtService.generateToken(user);
logger.info("사용자 {} 로그인 성공", username);
return new AuthResult(true, "로그인 성공", token);
}
}
기본 사용법
이제 이 대형 메서드를 리팩토링해 보겠습니다.
첫 번째 단계로 책임 영역을 파악합니다:
- 입력 유효성 검사
- 사용자 조회
- 계정 상태 확인
- 계정 잠금 확인 및 처리
- 비밀번호 확인
- 로그인 성공 처리
- 로그인 이벤트 기록
- 보안 검사
- JWT 토큰 생성
각 책임을 별도의 메서드로 추출합니다:
// 리팩토링 후: 여러 메서드로 분리됨
public class AuthenticationService {
public AuthResult authenticate(String username, String password, String ipAddress) {
// 입력 유효성 검사
AuthResult validationResult = validateInput(username, password);
if (!validationResult.isSuccess()) {
return validationResult;
}
// 사용자 조회
User user = findUser(username);
if (user == null) {
return createFailureResult("사용자명 또는 비밀번호가 잘못되었습니다.");
}
// 계정 상태 확인
AuthResult accountStatusResult = checkAccountStatus(user);
if (!accountStatusResult.isSuccess()) {
return accountStatusResult;
}
// 비밀번호 확인 및 계정 잠금 처리
AuthResult passwordResult = verifyPassword(user, password);
if (!passwordResult.isSuccess()) {
return passwordResult;
}
// 로그인 성공 처리
updateSuccessfulLogin(user, ipAddress);
// 로그인 이벤트 기록
recordLoginEvent(username, ipAddress, true);
// 보안 검사
checkSecurity(user, ipAddress);
// JWT 토큰 생성
String token = generateToken(user);
logger.info("사용자 {} 로그인 성공", username);
return new AuthResult(true, "로그인 성공", token);
}
private AuthResult validateInput(String username, String password) {
if (username == null || username.trim().isEmpty()) {
logger.warn("인증 실패: 사용자명이 비어 있음");
return new AuthResult(false, "사용자명이 필요합니다.");
}
if (password == null || password.isEmpty()) {
logger.warn("인증 실패: 비밀번호가 비어 있음");
return new AuthResult(false, "비밀번호가 필요합니다.");
}
return new AuthResult(true, "");
}
private User findUser(String username) {
return userRepository.findByUsername(username);
}
private AuthResult checkAccountStatus(User user) {
if (!user.isActive()) {
logger.warn("인증 실패: 사용자 {} 계정 비활성화됨", user.getUsername());
return new AuthResult(false, "계정이 비활성화되었습니다.");
}
if (user.isLocked()) {
return handleLockedAccount(user);
}
return new AuthResult(true, "");
}
private AuthResult handleLockedAccount(User user) {
if (user.getLockExpirationTime().after(new Date())) {
logger.warn("인증 실패: 사용자 {} 계정 잠김", user.getUsername());
return new AuthResult(false, "계정이 잠겼습니다. 나중에 다시 시도하세요.");
} else {
unlockAccount(user);
return new AuthResult(true, "");
}
}
private void unlockAccount(User user) {
user.setLocked(false);
user.setLoginAttempts(0);
userRepository.save(user);
}
// 나머지 메서드들도 계속 분리...
}
다음은 리팩토링 전후의 차이점을 표로 정리한 것입니다:
항목 | 리팩토링 전 | 리팩토링 후 |
---|---|---|
메서드 수 | 1개 (authenticate) | 10개 이상의 작은 메서드들 |
메서드 길이 | 약 100줄 | 주 메서드 25줄, 나머지 5-15줄 |
가독성 | 낮음 (모든 로직이 혼합) | 높음 (각 책임이 분리됨) |
테스트 용이성 | 어려움 (전체를 한번에 테스트) | 쉬움 (각 메서드를 독립적으로 테스트 가능) |
재사용성 | 낮음 | 높음 (분리된 메서드를 다른 곳에서 재사용 가능) |
유지보수성 | 어려움 | 용이함 (각 책임 영역 변경이 독립적) |
주의사항 및 팁 💡
⚠️ 이것만은 주의하세요!
과도한 메서드 분리
- 너무 작은 메서드들로 과도하게 분리하면 오히려 코드 이해가 어려워질 수 있습니다.
- 의미 있는 단위로 분리하세요.
메서드 이름 선택
- 추출한 메서드의 이름은 그 기능을 명확히 표현해야 합니다.
- "doSomething"과 같은 모호한 이름은 피하세요.
성능 고려
- 메서드 호출이 많아지면 약간의 성능 저하가 있을 수 있습니다.
- 대부분의 경우 가독성과 유지보수성 향상이 더 중요합니다.
💡 꿀팁
- IDE의 리팩토링 도구를 활용하세요. 대부분의 IDE는 자동 메서드 추출 기능을 제공합니다.
- "Extract Till You Drop" 원칙을 고려하세요. 메서드가 한 가지 일만 할 때까지 계속 추출합니다.
- 같이 변경되는 코드는 같은 메서드에 두고, 독립적으로 변경되는 코드는 분리하세요.
- 주석이 필요한 코드 블록은 좋은 메서드 추출 후보입니다.
- 분기문(if-else, switch)은 각 분기를 별도 메서드로 추출하는 것을 고려하세요.
마치며
지금까지 대형 메서드 분리 기법에 대해 알아보았습니다. 복잡한 메서드를 작고 명확한 단위로 분리하는 것은 코드 품질을 크게 향상시키는 중요한 리팩토링 기법입니다. 처음에는 시간이 더 소요될 수 있지만, 장기적으로는 유지보수 시간과 버그 발생 가능성을 크게 줄일 수 있습니다. 🚀
여러분의 코드에서 이러한 기법을 적용해 보시고, 더 깔끔하고 이해하기 쉬운 코드를 만들어 보세요!
혹시 궁금한 점이 있으시거나, 실제 적용 과정에서 어려움을 겪고 계신다면 댓글로 남겨주세요. 함께 해결책을 찾아보겠습니다! 😊
참고 자료 🔖
- Martin Fowler, "Refactoring: Improving the Design of Existing Code"
- Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship"
- https://refactoring.guru/extract-method
- https://www.codesee.io/learning-center/code-refactoring
#코드리팩토링 #대형메서드분리 #클린코드 #코드품질
'800===Dev Docs and License > Clean Code' 카테고리의 다른 글
파이썬 코드 리팩토링 마스터 가이드 - 코드 테스트와 유지보수성 향상 🧹✨ (0) | 2025.01.07 |
---|---|
파이썬 코드 리팩토링 마스터 가이드 - 성능 최적화 핵심 가이드 🚀 (0) | 2025.01.07 |
파이썬 코드 리팩토링 마스터 가이드: 코드 구조 개선 🛠️ (0) | 2025.01.07 |
파이썬 코드 리팩토링의 핵심 가이드 🎯 (0) | 2025.01.07 |
SOLID 원칙 - 객체지향 설계의 완벽 가이드 🧩 (1) | 2024.12.06 |