코드 한 줄의 기록

Java 개발자를 위한 필수 가이드: main 메소드 의존 습관을 버리고 깔끔한 코드로 나아가는 길 본문

JAVA

Java 개발자를 위한 필수 가이드: main 메소드 의존 습관을 버리고 깔끔한 코드로 나아가는 길

CodeByJin 2025. 9. 11. 08:57
반응형

많은 Java 초보 개발자들이 범하는 가장 흔하면서도 심각한 실수 중 하나는 모든 코드를 main 메소드 안에 몰아넣는 것입니다. 이는 단순히 '나쁜 습관'을 넘어서 프로젝트의 확장성과 유지보수성을 크게 저해하는 안티 패턴입니다.

프로그래밍을 처음 배울 때는 "Hello World"부터 시작해서 모든 로직을 main 메소드에 넣는 것이 자연스럽게 느껴집니다. 하지만 이러한 습관이 굳어지면, 수천 줄의 코드가 하나의 메소드에 집중되고, 결국 아무도 건드리고 싶지 않은 '레거시 괴물'이 탄생하게 됩니다.

main 메소드에 모든 것을 넣는 습관이 위험한 이유

1. 단일 책임 원칙(SRP) 위반의 심각성
단일 책임 원칙은 객체지향 프로그래밍의 핵심 원칙 중 하나로, "하나의 클래스는 하나의 책임만 가져야 한다"는 것입니다. main 메소드에 모든 코드를 넣는다는 것은 이 원칙을 정면으로 위반하는 행위입니다.

// 나쁜 예시: 모든 로직이 main 메소드에 집중
public class BadExample {
    public static void main(String[] args) {
        // 사용자 입력 처리
        Scanner scanner = new Scanner(System.in);
        System.out.println("이름을 입력하세요:");
        String name = scanner.nextLine();

        // 데이터 검증
        if (name == null || name.trim().isEmpty()) {
            System.out.println("유효하지 않은 이름입니다.");
            return;
        }
        
        // 비즈니스 로직
        String processedName = name.trim().toLowerCase();
        String greeting = "안녕하세요, " + processedName + "님!";

        // 데이터베이스 처리 (가상의 코드)
        // ... 수백 줄의 데이터베이스 로직
        
        // 결과 출력
        System.out.println(greeting);

        // 파일 처리
        // ... 수백 줄의 파일 처리 로직

		// 에러 핸들링
        // ... 복잡한 예외 처리 로직

    }
}

 
이런 코드는 다음과 같은 문제점들을 가집니다:

  • 가독성 저하: 코드가 길어질수록 전체 흐름을 파악하기 어려워집니다.
  • 테스트 불가능: 개별 기능을 단위 테스트할 수 없습니다.
  • 재사용성 제로: 특정 기능만 다른 곳에서 사용할 수 없습니다.
  • 유지보수 지옥: 하나의 기능을 수정하려면 전체 메소드를 이해해야 합니다.

2. 코드 복잡도 증가와 인지 부하
인간의 뇌는 한 번에 처리할 수 있는 정보의 양이 제한되어 있습니다. 긴 메소드는 이러한 인지적 한계를 넘어서며, 개발자의 정신적 부담을 급격히 증가시킵니다.

// 복잡도가 높은 main 메소드 예시
public static void main(String[] args) {
    // 변수 선언 (10여개)
    String input1, input2, input3, result1, result2;
    int count1, count2, sum, average;
    boolean flag1, flag2, isValid;

    // 중첩된 조건문 (5단계 깊이)
    if (args.length > 0) {
        if (args[0].equals("process")) {
            for (int i = 0; i < 100; i++) {
                if (i % 2 == 0) {
                    if (i > 50) {
                        // 수십 줄의 로직...
                    }
                }
            }
        }
    }
    // ... 수백 줄 더 계속...
}

 
3. 디버깅과 오류 추적의 악몽
모든 로직이 하나의 메소드에 집중되어 있으면, 오류가 발생했을 때 문제의 원인을 찾는 것이 극도로 어려워집니다. 스택 트레이스는 항상 같은 메소드를 가리키고, 실제 문제가 되는 로직의 범위를 좁히는 것이 불가능합니다.

Exception in thread "main" java.lang.NullPointerException at BadExample.main(BadExample.java:847)

 
위와 같은 에러 메시지에서 847번째 줄이 문제라고 나와도, 실제로는 200번째 줄에서 잘못 초기화된 변수 때문일 수 있습니다. 이런 상황에서는 전체 메소드를 처음부터 끝까지 디버깅해야 합니다.

함수 분리가 가져다주는 혁명적 변화

1. 가독성과 이해도의 극적 향상
함수를 적절히 분리하면, 코드가 마치 잘 쓰인 소설처럼 자연스럽게 읽힙니다. 각 함수의 이름이 그 자체로 문서 역할을 하며, 전체 프로그램의 흐름을 한눈에 파악할 수 있게 됩니다.

// 개선된 예시: 적절한 함수 분리
public class GoodExample {
    public static void main(String[] args) {
        String userName = getUserInput();

        if (isValidName(userName)) {
            String processedName = processUserName(userName);
            saveUserToDatabase(processedName);
            displayWelcomeMessage(processedName);

        } else {
            displayErrorMessage();
        }
    }

	private static String getUserInput() {
	    Scanner scanner = new Scanner(System.in);
	    System.out.println("이름을 입력하세요:");
        return scanner.nextLine();
    }

    private static boolean isValidName(String name) {
        return name != null && !name.trim().isEmpty();
    }

    private static String processUserName(String name) {
        return name.trim().toLowerCase();
    }

    private static void saveUserToDatabase(String name) {
        // 데이터베이스 저장 로직
        System.out.println("사용자 " + name + "을(를) 데이터베이스에 저장했습니다.");
    }

    private static void displayWelcomeMessage(String name) {
        System.out.println("안녕하세요, " + name + "님!");
    }

    private static void displayErrorMessage() {
        System.out.println("유효하지 않은 이름입니다.");
    }
}

 
이제 main 메소드만 봐도 프로그램이 무엇을 하는지 명확하게 알 수 있습니다:

  1. 사용자 입력을 받는다
  2. 입력이 유효한지 검증한다
  3. 이름을 처리한다
  4. 데이터베이스에 저장한다
  5. 환영 메시지를 출력한다

2. 테스트 가능성의 확보
함수 분리의 가장 큰 장점 중 하나는 개별 기능에 대한 단위 테스트가 가능해진다는 것입니다. 이는 코드의 신뢰성을 크게 향상시킵니다.

@Test
public void testIsValidName() {
    assertTrue(GoodExample.isValidName("김철수"));
    assertFalse(GoodExample.isValidName(""));
    assertFalse(GoodExample.isValidName(null));
    assertFalse(GoodExample.isValidName("   "));
}

@Test
public void testProcessUserName() {
    assertEquals("kim", GoodExample.processUserName("  KIM  "));
    assertEquals("park", GoodExample.processUserName("Park"));
}

 
3. 코드 재사용성과 모듈화
분리된 함수들은 다른 클래스나 프로젝트에서도 쉽게 재사용할 수 있습니다. 이는 개발 효율성을 크게 향상시킵니다.

public class UserService {
    public static boolean isValidEmail(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }

	public static String formatPhoneNumber(String phone) {
    return phone.replaceAll("[^0-9]", "")
                   .replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1-$2-$3");
    }
}

public class OrderService {
    public void processOrder() {
        String email = getCustomerEmail();
        if (UserService.isValidEmail(email)) {
            // 주문 처리 로직
        }
    }
}

 
4. 디버깅과 오류 추적의 혁신
함수가 적절히 분리되어 있으면, 오류가 발생했을 때 정확한 위치를 즉시 파악할 수 있습니다. 스택 트레이스가 구체적인 함수 이름을 보여주므로, 문제의 범위를 쉽게 좁힐 수 있습니다.

Exception in thread "main" java.lang.IllegalArgumentException: Invalid email format
    at UserService.isValidEmail(UserService.java:15)
    at OrderService.processOrder(OrderService.java:23)
    at Main.main(Main.java:8)

실전 리팩토링: 단계별 함수 분리 방법

1. Extract Method 기법 활용
Extract Method는 가장 기본적이면서도 강력한 리팩토링 기법입니다. 긴 메소드에서 의미 있는 코드 블록을 찾아 별도의 메소드로 추출하는 것입니다.

// 리팩토링 전
public static void main(String[] args) {
    double[] prices = {1000, 2000, 3000, 4000, 5000};
    double subtotal = 0;

	for (double price : prices) {
        subtotal += price;
    }

    double discount = subtotal * 0.1;
	double tax = (subtotal - discount) * 0.1;
	double total = subtotal - discount + tax;

	System.out.println("소계: " + subtotal);
	System.out.println("할인: " + discount);
	System.out.println("세금: " + tax);
	System.out.println("총액: " + total);
}

// 리팩토링 후
public class OrderCalculator {
    public static void main(String[] args) {
        double[] prices = {1000, 2000, 3000, 4000, 5000};
        double subtotal = calculateSubtotal(prices);
        double discount = calculateDiscount(subtotal);
        double tax = calculateTax(subtotal, discount);
        double total = calculateTotal(subtotal, discount, tax);

        displayOrderSummary(subtotal, discount, tax, total);
    }

    private static double calculateSubtotal(double[] prices) {
        double subtotal = 0;

		for (double price : prices) {
		    subtotal += price;
		}

        return subtotal;
    }

    private static double calculateDiscount(double subtotal) {
        return subtotal * 0.1;
    }    

    private static double calculateTax(double subtotal, double discount) {
        return (subtotal - discount) * 0.1;
    }    

    private static double calculateTotal(double subtotal, double discount, double tax) {
        return subtotal - discount + tax;
    }    

    private static void displayOrderSummary(double subtotal, double discount, double tax, double total) {
        System.out.println("소계: " + subtotal);
        System.out.println("할인: " + discount);
        System.out.println("세금: " + tax);
        System.out.println("총액: " + total);
    }
}

객체지향 프로그래밍으로의 진화

1. 절차지향에서 객체지향으로
함수 분리에 익숙해졌다면, 다음 단계는 관련된 함수들을 클래스로 그룹화하는 것입니다. 이는 코드를 더욱 체계적으로 조직화하고 유지보수를 용이하게 만듭니다.

// 절차지향 스타일의 함수 분리
public class MathUtils {
    public static double calculateArea(double radius) {
        return Math.PI * radius * radius;
    }

	public static double calculateCircumference(double radius) {
        return 2 * Math.PI * radius;
    }
}

// 객체지향 스타일로 진화
public class Circle {
    private double radius;

    public Circle(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("반지름은 0보다 커야 합니다.");
        }

        this.radius = radius;
    }

    public double getArea() {
        return Math.PI * radius * radius;
    }

    public double getCircumference() {
        return 2 * Math.PI * radius;
    }    

    public double getRadius() {
        return radius;
    }    

    public void setRadius(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("반지름은 0보다 커야 합니다.");
        }

        this.radius = radius;
    }
}

 
2. 캡슐화와 정보 은닉의 활용
객체지향의 핵심 원리 중 하나인 캡슐화를 활용하면, 데이터와 해당 데이터를 조작하는 메소드를 하나의 단위로 묶을 수 있습니다.

public class BankAccount {
    private String accountNumber;
    private double balance;
    private String ownerName;

    public BankAccount(String accountNumber, String ownerName, double initialBalance) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = initialBalance;
    }

    public boolean deposit(double amount) {
        if (isValidAmount(amount)) {
            balance += amount;
            logTransaction("입금", amount);
            return true;
        }

        return false;
    }

    public boolean withdraw(double amount) {
        if (isValidAmount(amount) && hasSufficientBalance(amount)) {
            balance -= amount;
            logTransaction("출금", amount);
            return true;
        }

        return false;
    }

    private boolean isValidAmount(double amount) {
        return amount > 0;
    }

    private boolean hasSufficientBalance(double amount) {
        return balance >= amount;
    }

    private void logTransaction(String type, double amount) {
        System.out.println(String.format("%s: %.2f원, 잔액: %.2f원", type, amount, balance));
    }

    public double getBalance() { return balance; }

    public String getAccountNumber() { return accountNumber; }

    public String getOwnerName() { return ownerName; }

}

현대적 Java 개발 패턴과 도구 활용

1. 함수형 프로그래밍 패러다임 도입

// 기존의 절차적 방식
public static void processUsers(List users) {
    List activeUsers = new ArrayList<>();

    for (User user : users) {
        if (user.isActive()) {
            activeUsers.add(user);
        }
    }

    List emails = new ArrayList<>();

	for (User user : activeUsers) {
	    emails.add(user.getEmail());
    }

	Collections.sort(emails);

    for (String email : emails) {
        System.out.println(email);
    }
}

// 함수형 스타일
public static void processUsers(List users) {
    users.stream()
         .filter(User::isActive)
         .map(User::getEmail)
         .sorted()
         .forEach(System.out::println);
}

 
2. 디자인 패턴의 활용

// Strategy 패턴을 활용한 할인 정책 구현
interface DiscountStrategy {
    double calculateDiscount(double amount);
}

class NoDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return 0;
    }
}

class PercentageDiscount implements DiscountStrategy {
    private final double percentage;

	public PercentageDiscount(double percentage) {
        this.percentage = percentage;
    }

    @Override
    public double calculateDiscount(double amount) {
        return amount * (percentage / 100);
    }
}

class FixedAmountDiscount implements DiscountStrategy {
    private final double fixedAmount;

    public FixedAmountDiscount(double fixedAmount) {
        this.fixedAmount = fixedAmount;
    }

    @Override
    public double calculateDiscount(double amount) {
        return Math.min(fixedAmount, amount);
    }
}

class OrderProcessor {
    private DiscountStrategy discountStrategy;

    public OrderProcessor(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double processOrder(double amount) {
        double discount = discountStrategy.calculateDiscount(amount);
        return amount - discount;
    }

    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }
}

 
3. 의존성 주입과 제어 역전

// 의존성 주입 전: 강한 결합
class EmailService {
    private SMTPClient smtpClient;

	public EmailService() {
        this.smtpClient = new SMTPClient();
    }

    public void sendEmail(String to, String subject, String body) {
        smtpClient.send(to, subject, body);
    }
}

// 의존성 주입 후: 느슨한 결합
interface EmailClient {
    void send(String to, String subject, String body);
}

class SMTPClient implements EmailClient {
    @Override
    public void send(String to, String subject, String body) {
        System.out.println("SMTP로 전송: " + to);
    }
}

class MockEmailClient implements EmailClient {
    @Override
    public void send(String to, String subject, String body) {
        System.out.println("모의 전송: " + to);
    }
}

class EmailService {
    private final EmailClient emailClient;

    public EmailService(EmailClient emailClient) {
        this.emailClient = emailClient;
    }

    public void sendEmail(String to, String subject, String body) {
        validateEmail(to);
        emailClient.send(to, subject, body);
        logEmailSent(to);
    }

	private void validateEmail(String email) {
        if (!email.contains("@")) {
            throw new IllegalArgumentException("유효하지 않은 이메일 주소입니다.");
        }
    }

    private void logEmailSent(String to) {
        System.out.println("이메일 전송 로그: " + to + " - " + new Date());
    }
}

public class EmailDemo {
    public static void main(String[] args) {
        EmailService realService = new EmailService(new SMTPClient());
        EmailService testService = new EmailService(new MockEmailClient());
        realService.sendEmail("user@example.com", "안녕하세요", "테스트 메시지");
    }
}

더 나은 Java 개발자로 성장하기

main 메소드에 모든 코드를 몰아넣는 습관은 단순한 '나쁜 습관' 이상의 문제입니다. 이는 소프트웨어의 확장성, 유지보수성, 테스트 가능성을 근본적으로 해치는 안티 패턴입니다.
하지만 이 글에서 제시한 체계적인 접근 방식을 따른다면, 누구나 깔끔하고 유지보수가 용이한 코드를 작성할 수 있습니다:

  1. 단일 책임 원칙을 기반으로 한 함수 분리
  2. Extract Method, Decompose Conditional 등의 체계적 리팩토링 기법 적용
  3. 객체지향 원리를 활용한 코드 구조화
  4. 테스트 주도 개발과 지속적인 품질 개선
  5. 팀 단위 코딩 컨벤션과 자동화 도구 활용

이러한 변화는 하루아침에 이루어지지 않습니다. 하지만 점진적이고 지속적인 개선을 통해, 여러분도 다른 개발자들이 존경하는 고품질 코드를 작성하는 전문가가 될 수 있습니다.

기억하세요. 좋은 코드는 작성할 때보다 읽을 때를 고려해서 만들어집니다. 6개월 후의 자신과 팀 동료들이 여러분의 코드를 보고 미소 지을 수 있도록, 오늘부터 함수 분리와 체계적인 코드 구조화를 실천해보시기 바랍니다.

Java 개발의 여정에서 가장 중요한 것은 지속적인 학습과 개선입니다. main 메소드의 굴레에서 벗어나 진정한 객체지향 개발자로 성장하는 그 순간, 여러분의 코딩 인생이 완전히 달라질 것입니다.

 

자바 변수 초기화와 스코프(Scope) 완벽 정리: 실무에서 놓치기 쉬운 함정들

자바를 처음 배울 때 많은 개발자들이 가장 기본적이면서도 중요한 개념을 놓치곤 합니다. 바로 변수 초기화와 스코프(Scope)입니다. "이런 기초적인 걸 왜 모르냐"고 하실 수도 있지만, 실제로는

byteandbit.tistory.com

반응형