풀스택 개발자 과정 14일차

너구·2026년 5월 26일

풀스택 성장과정

목록 보기
14/79

오늘은 리팩토링을 진행해보며 여태 배워왔던 것들을 돌아보고 바른 코딩이란 무엇인가에 대해 배워보는 시간을 가져보았다.


리팩토링(Refactoring)

리팩토링이란 기능은 그대로 두고 내부를 개선하는 것이다.
코드를 읽기 쉽게 만들고 중복을 제거하며 유지보수가 쉬워지게 된다.


리팩토링 예제 (1)

나쁜 예시

class Person {
    private String name;
    private int age;

    Person (String name, int age) {
        this.name = name;
        this.age = age;
    }

    String getName() {
        return name;
    }

    int getAge() {
        return age;
    }
}

public class Main {
    public static void main(String[] args) {
        
        Person person = new Person("짱구", 5);

        // 성인 판단 여부를 클래스 밖에서 하고 있다.
        // 그런데 만약 성인 판단의 기준을 바꿔야 하는 상황이 생기면?
        // 게다가 아래의 코드가 소스 코드에서 수백 수천줄이라면?
        // 개선 방법: 성인 판단을 내부에서 하고 결과만 돌려주기
        // 그러면 성인 판단의 기준이 바뀌어도 클래스 내부만 바꾸면 된다
        if (person.getAge() < 18 ) {
            System.out.println("어린이는 입장불가");
            return;
        }
        System.out.println("잘 오셨습니다");
    }
}

이 코드는 캡슐화를 제대로 활용하지 못한 나쁜 예시의 코드이다.
먼저 Person 클래스에 private을 사용하고 Getter를 통해 외부에서 값을 조회할 수 있게 만든 것은 괜찮은 구조이다.

하지만 문제는 Main에 있다.
if(person.getAger() < 18)을 사용해 성인 판단을 클래스 외부에서 직접 하고 있다.
즉 Person 객체가 자신의 상태를 스스로 판단하지 못하고 외부 코드가 직접 판단하고 있는 구조라는 것이다.

이 구조의 문제점은 성인 기준이 바뀌게 된다면 조건을 수정해야 할 수도 있다라는 점이다.
만약 이런 코드가 프로젝트 전체에 수십 개, 수백 개가 있따면 유지보수가 굉장히 어려워진다.

또 getAge()로 내부 데이터를 꺼내서 직접 사용하는 것은 캡슐화가 약해지는 원인이 된다.
객체지향에서는 데이터를 꺼내서 판단하기 보다 객체가 스스로 판단하게 만드는 구조를 더 좋은 코드라고 보고 있다.

그렇다면 좋은 코드는 어떻게 쓰면 될까?

좋은 예시

class Person {
    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 성인 판단하고 결과만 돌려줍니다
    boolean isAdult() {
        // 나이가 아니라 외부 인증 방식으로 바뀌어도
        // 여기만 수정하면 됩니다
        return age >= 18;
    }
}

public class Main {
    public static void main(String[] args) {
        
        Person person = new Person("짱구", 5);
        
        if (!person.isAdult()) {
            System.out.println("어린이는 입장 불가");
            return;
        }

        System.out.println("잘 오셨습니다");
    }
    
}

이번에는 성인 판단 로직을 Person 클래스 내부로 옮겨보았다.
voolean isAdult() 메서드를 만들어 객체가 자신의 상태를 스스로 판단하도록 수정하였다.
이전 코드에서는 외부에서 직접 나이를 비교하였지만 이번 코드에서는 person.isAdult()처럼 결과만 받아 사용하도록 변경하였다.
이렇게 하면 성인 기준이 18세에서 20세로 바뀌거나 나이 대신 인증 방식으로 변경되더라도 Person 클래스 내부만 수정하면 된다.
즉 객체가 자신의 데이터를 직접 관리하고 판단하게 만들면서 캡슐화가 더 잘 이루어진 구조라고 볼 수 있다.
또 외부에서는 내부로직을 몰라도 되기 때문에 코드의 결합도도 낮아지고 유지보수도 훨씬 쉬워진다.


리팩토링 예제 (2)

이번에도 나쁜 예시 먼저 확인해보겠다.

나쁜 예시

// 인증처리기
class PasswordAuthProcessor { // 비밀번호 인증 처리기
    void checkPassword() {
        System.out.println("비밀번호 검증중");
    }
}

class FingerPrintAuthenticator { // 지문인증 처리기
    void processAuth() {
        System.out.println("지문 검증중");
    }
}

class FaceIDProcessor { // 얼굴인증 처리기
    void authenticate() {
        System.out.println("얼굴 검증중");
    }
}
// 시스템 관리자 - 핵심 로직을 담당합니다.
class SystemService {
    // 시스템을 활성화 처리하는 함수
    void activateSystem(String authMethods) { // authMethods: 사용자가 요청한 인증 수단
        System.out.println("시스템 작동 준비중");
        System.out.println("자원 확보중...");

        // 인증절차
        // 시스템 관리자가 모든 인증수단을 알고있어야 하는 상황
        if ("password".equals(authMethods)) {
            PasswordAuthProcessor passwordAuthProcessor = new PasswordAuthProcessor();
            passwordAuthProcessor.checkPassword();
        } else if ("fingerprint".equals(authMethods)) {
            FingerPrintAuthenticator fingerPrintAuthenticator = new FingerPrintAuthenticator();
            fingerPrintAuthenticator.processAuth();
        } else if ("face-id".equals(authMethods)) {
            FaceIDProcessor faceIDProcessor = new FaceIDProcessor();
            faceIDProcessor.authenticate();
        } else {
            System.out.println("알 수 없는 인증 방식입니다.");
            return;
        }

        System.out.println("인증 완료");
        System.out.println("시스템이 활성화되었습니다");
    }
}
public class Main {
    public static void main(String[] args) {
        SystemService systemService = new SystemService();
        // 사용자가 얼굴인식 방식으로 시스템 활성화를 요청함
        systemService.activateSystem("face-id");
    }
}

이 코드는 인증 기능은 동작하지만 객체지향 관점에서는 좋지 않은 구조의 코드이다.
가장 큰 문제는 SystemService가 모든 인증 방식을 전부 알고 있어야 한다라는 점이다.
현재 activateSystem() 안을 보면 if("password".equals(authMethods))처럼 인증방식마다 조건문이 계속 나뉘고 있다.
즉 비밀번호 인증, 지문 인증, 얼굴 인증과 같은 모든 인증 처리기를 SystemService가 직접 관리하고 있는 상태인 것이다.

이 구조의 문제점은 새로운 인증방식이 추가가 될 때마다 같은 기능을 추가하려면 SystemService 코드를 계속 수정해야 한다라는 점이다.
기능을 추가할 때 기존 핵심 로직까지 건드려야 된다라는 문제가 생긴다.
이것은 OCP(개방-폐쇄 원칙)위반이라고 볼 수 있다.

또 PasswordAuthProcessor, FingerPrintAuthenticator, FaceIDProcessor 처럼 클래스마다 메서드 이름도 전부 다르다.
checkPassword(), processAuth(), authenticate() 이렇게 제각각이라 공통 규직이 없다.
그래서 다형성도 사용할 수 없다.

결국 SystemService는 인증 방식도 클래스도 메서드 이름도 다 알아야 한다.
즉 핵심 로직에 너무 많은 책임이 몰려있는 구조라는 거다.

그렇다면 좋은 코드는 어떻게 작성해야 할까?

좋은 예시

// 인증기 인터페이스 (기준, 규격)
interface Authenticator {
    void authenticate();
}

// 구현 클래스
class PasswordAuthenticator implements Authenticator {
    // 메서드 이름이 표준화 됩니다
    public void authenticate() {
        System.out.println("비밀번호 검증중");
    }
}

class FingerPrintAuthenticator implements Authenticator {
    public void authenticate() {
        System.out.println("지문 검증중");
    }
}

class FaceIDAuthentivator implements Authenticator {
    public void authenticate() {
        System.out.println("얼굴 검증중");
    }
}

// 시스템 관리자
class SystemService {
    // 인증기 인터페이스에 의존합니다 -> 어떤 인증기도 주입받을 수 있음
    Authenticator authenticator;

    SystemService(Authenticator authenticator) {
        this.authenticator = authenticator;
    }

    // 시스템을 활성화하는 함수
    void activateSystem() {
        System.out.println("시스템 작동 준비중");
        System.out.println("자원 확보중...");

        // 인증 처리중: 만능 인증 처리기(authenticator)
        authenticator.authenticate();

        System.out.println("인증 완료");
        System.out.println("시스템이 활성화되었습니다");
    }
}

public class Main {
    public static void main(String[] args) {
        // 사용자의 인증방식
        String authMethods = "face-id";
        Authenticator authenticator;

        // 인증 방식에 따라 적절한 인증기를 생성합니다
        if ("password".equals(authMethods)) {
            authenticator = new PasswordAuthenticator();
        } else if ("fingerprint".equals(authMethods)) {
            authenticator = new FingerPrintAuthenticator();
        } else if ("face-id".equals(authMethods)) {
            authenticator = new FaceIDAuthentivator();
        } else {
            System.out.println("알 수 없는 인증 방식입니다.");
            return;
        }

        // 생성한 인증기를 시스템 관리자에게 주입합니다
        SystemService systemService = new SystemService(authenticator);
        // 시스템 활성화
        systemService.activateSystem();
    }
}

이번 코드는 이전의 나쁜 예시를 인터페이스와 다형성을 활용해 개선한 좋은 구조의 코드이다.

이전 나쁜 예시에서는 SystemService가 비밀번호, 지문, 얼굴 인증 같은 모든 인증 클래스와 메서드를 직접 알고 있어야 했다.
예를 들면 passwordAuthProcessor.checkPassword();, fingerPrintAuthenticator.processAuth();, faceIDProcessor.authenticate(); 처럼 클래스마다 메서드 이름도 전부 달랐다.

그래서 SystemServeice는 어떤 인증 클래스가 있는지 어떤 메서드를 호출해야 하는지 어떤 객체를 생성해야 하는지 전부 알고 있어야 했다.
즉 핵심 로직인 시스템 관리자가 인증 처리 세부사항까지 전부 책임지는 구조였던 것이다.

또 새로운 인증 방식이 추가가된다면 if else문도 계속 수정해야 했다.
예를들어 홍채 인증, 카카오 인증 같은 기능이 추가될 때마다 SystemService 내부를 계속 건드려야했다.
즉 확장에 약한 구조라고 볼 수 있다.

하지만 이번 좋은 예시에서는 interface를 만들어 인증 처리 규칙을 통일하였다.
즉 인증 처리기라면 authenticate() 메서드를 가져야한다라는 기준을 만든 것이다.
그래서 모든 인증 클래서가 authenticate() 메서드를 동일하게 사용하게 되었다.
이 부분 덕분에 SystemService는 authenticator.authenticate(); 한 줄만 호출하면 된다.
즉 시스템 관리자는 인증 방식이 무엇인지, 내부에서 어떻게 처리가 되는지를 몰라도 되는 것이다.

그리고 이번 코드의 if문은 이전 코드와 역할이 다르다.
이전 나쁜 예시에서는 if문이 인증 처리까지 담당하였지만 이번 코드에서는 if문은 어떤 객체를 사용할지 선택만 한다라는 차이가 있다.
즉 authenticator = new FaceIDAuthentivator(); 처럼 상황에 맞는 객체를 생성만 하고 실제 인증 처리는 authenticator.authenticate(); 인터페이스를 통해 실행된다.

즉 이번 코드는 인터페이스, 다형성, 의존성 주입 개념을 잘 활용해서 객체지향적으로 훨씬 개선된 좋은 코드라고 볼 수가 있다.


마무리

오늘은 리팩토링과 객체지향 설계에 대해 배우는 시간을 가졌다.
처음에는 단순히 기능만 동작하면 되는줄 알았는데, 코드를 어떻게 구조적으로 작성하느냐가 정말 중요하다는 것을 느꼈다.
특히 인터페이스와 다형성을 활용해 인증 시스템을 개선하는 과정이 가장 흥미로웠다.

기존에는 if else문 안에서 모든 인증 로직을 직접 처리했지만, 리팩토링 이후에는 인터페이스를 통해 역할을 분리하고 객체끼리 유연하게 연결할 수 있게 되었다.
덕분에 새로운 기능이 추가되어도 기존 코드를 크게 수정하지 않아도 된다는 점이 인상깊었다.

또 객체지향의 캡슐화, 상속, 오버라이딩, 다형성, 의존성 주입 같은 개념들이 실제 코드에서 어떻게 사용되는지도 이해할 수 있었다.

오늘 배운 내용 덕분에 동작하는 코드와 좋은 코드는 다르다는 것을 알게 되었고 앞으로는 유지보수와 확장성까지 생각하면서 코드를 작성해야겠다라는 생각이 들었다.

0개의 댓글