본문 바로가기
Spring/이론

Static 메서드, 언제 사용하고 언제 피해야 할까?

by JuNo_12 2025. 6. 16.

들어가며

Spring Boot 프로젝트를 진행하다 보면 ApiResponse.success(data)와 같은 static 메서드를 자주 접하게 됩니다. 하지만 언제 static 메서드를 사용해야 하고, 언제 일반 인스턴스 메서드를 사용해야 하는지 명확한 기준을 정하기는 쉽지 않습니다.

이번 글에서는 실제 코드 예시를 통해 static 메서드의 적절한 사용 시기와 피해야 할 상황을 알아보겠습니다.


Static 메서드를 사용하는 이유

 

1. 편의성과 가독성

먼저 ApiResponse 클래스를 예로 들어보겠습니다.

// Static 메서드가 없다면
@PostMapping("/users")
public ResponseEntity<ApiResponse<User>> createUser() {
    User user = userService.createUser();
    
    // 매번 객체를 생성해야 함
    ApiResponse<User> apiResponse = new ApiResponse<>();
    return ResponseEntity.ok(apiResponse.success(user, "사용자가 생성되었습니다"));
}

// Static 메서드를 사용하면
@PostMapping("/users")
public ResponseEntity<ApiResponse<User>> createUser() {
    User user = userService.createUser();
    
    // 바로 사용 가능
    return ResponseEntity.ok(ApiResponse.success(user, "사용자가 생성되었습니다"));
}

Static 메서드를 사용하면 불필요한 임시 객체 생성 없이 바로 원하는 결과를 얻을 수 있습니다.

 

2. 팩토리 메서드 패턴 구현

ApiResponse 클래스는 팩토리 메서드 패턴을 구현한 대표적인 예시입니다.

public class ApiResponse<T> {
    
    // 생성자를 private으로 숨김
    private ApiResponse(T data, String message) {
        this.success = true;
        this.message = message;
        this.data = data;
        this.timestamp = LocalDateTime.now();
    }
    
    // Static 팩토리 메서드만 제공
    public static <T> ApiResponse<T> success(T data, String message) {
        return new ApiResponse<>(data, message);
    }
    
    public static <T> ApiResponse<T> failure(String message) {
        return new ApiResponse<>(false, message, null, LocalDateTime.now());
    }
}

 

이런 패턴은 Java 표준 라이브러리에서도 자주 사용됩니다.

Optional.of(value)              // Optional 생성
Collections.emptyList()         // 빈 리스트 생성
LocalDateTime.now()            // 현재 시간 객체 생성
String.valueOf(123)            // String 변환

Static 메서드를 피해야 하는 경우

 

1. 상태를 가지는 클래스

// 잘못된 예시 - 위험한 static 사용
public class UserService {
    
    private static UserRepository userRepository; // 위험!
    private static int userCount = 0;             // 매우 위험!
    
    public static User createUser(String name) {
        userCount++; // 멀티스레드 환경에서 동시성 문제 발생
        return userRepository.save(new User(name));
    }
}

Static 변수는 모든 스레드가 공유하기 때문에 동시성 문제가 발생할 수 있습니다.

 

2. Spring의 의존성 주입이 필요한 경우

// 잘못된 예시
public class EmailService {
    
    // Spring DI를 사용할 수 없음
    public static void sendEmail(String to, String subject) {
        // JavaMailSender를 어떻게 주입받을까?
        // 설정값들은 어떻게 가져올까?
    }
}

// 올바른 예시
@Service
public class EmailService {
    
    @Autowired
    private JavaMailSender mailSender;
    
    @Value("${mail.from}")
    private String fromAddress;
    
    public void sendEmail(String to, String subject) {
        // 의존성 주입을 통해 필요한 객체들을 사용
    }
}

3. 테스트하기 어려운 경우

// 테스트하기 어려운 static 메서드
public class PaymentProcessor {
    
    public static boolean processPayment(Payment payment) {
        // 외부 API 호출 - 테스트 시 모킹 불가능
        ExternalPaymentApi.charge(payment.getAmount());
        return true;
    }
}

// 테스트 코드에서
@Test
void testPayment() {
    // PaymentProcessor.processPayment()를 모킹할 수 없음
    // 실제 결제 API가 호출됨
}

Static 메서드는 모킹이 어렵기 때문에 외부 의존성이 있는 경우 테스트가 복잡해집니다.


Static 메서드 사용 판단 기준

다음 체크리스트를 통해 static 메서드 사용 여부를 판단할 수 있습니다.

Static 사용이 적절한 경우

  1. 상태가 없는가? (Stateless)
    • 인스턴스 변수가 없음
    • 메서드가 순수 함수임 (같은 입력 → 같은 출력)
  2. 의존성이 없는가?
    • 다른 Bean을 주입받을 필요 없음
    • 외부 리소스에 의존하지 않음
  3. 테스트하기 쉬운가?
    • 모킹이 필요 없음
    • 부작용이 없음
  4. 유틸리티 성격인가?
    • 도구적 성격의 메서드
    • 여러 곳에서 공통으로 사용

 

적절한 Static 메서드 예시

// 순수 유틸리티 함수
public class StringUtils {
    
    public static boolean isEmpty(String str) {
        return str == null || str.trim().isEmpty();
    }
    
    public static String capitalize(String str) {
        if (isEmpty(str)) return str;
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }
}

// 수학적 계산
public class DateUtils {
    
    public static LocalDateTime toKoreanTime(LocalDateTime utc) {
        return utc.plusHours(9);
    }
    
    public static boolean isWeekend(LocalDate date) {
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
    }
}

// 검증 로직
public class ValidationUtils {
    
    public static boolean isValidEmail(String email) {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
    
    public static boolean isValidPassword(String password) {
        return password != null && 
               password.length() >= 8 && 
               password.matches(".*[A-Z].*") && 
               password.matches(".*[a-z].*") && 
               password.matches(".*\\d.*");
    }
}

Static 사용이 권장되는 패턴

// 팩토리 메서드
ApiResponse.success(data)
Optional.of(value)
Collections.emptyList()

// 유틸리티 함수
StringUtils.isEmpty(str)
DateUtils.format(date)
ValidationUtils.isEmail(email)

// 상수 관리
Constants.MAX_RETRY_COUNT
ErrorCodes.INVALID_INPUT

 

Static 사용을 피해야 하는 패턴

// Spring 서비스 로직
@Service UserService        // static 사용 금지
@Repository UserRepository  // static 사용 금지
@Controller UserController  // static 사용 금지

// 상태를 가지는 클래스
class ShoppingCart          // static 사용 금지
class GameSession          // static 사용 금지
class DatabaseConnection   // static 사용 금지

마무리

Static 메서드는 강력한 도구이지만, 모든 상황에 적합하지는 않습니다.

사용이 적절한 경우:

  • 상태가 없는 순수 함수
  • 의존성이 없는 유틸리티 메서드
  • 팩토리 메서드 패턴

사용을 피해야 하는 경우:

  • Spring Bean 기능이 필요한 경우
  • 상태를 가지는 로직
  • 테스트하기 어려운 외부 의존성

 

가장 중요한 것은 코드의 목적과 상황에 맞게 적절히 선택해야한다는 것을 다시 느낄 수 있었습니다.