풀스택 개발자 과정 16일차

너구·2026년 5월 28일

풀스택 성장과정

목록 보기
16/79

오늘도 문제 풀이를 하며 조금 더 심화된 코드를 작성하고 풀이해 보았다.


템플릿 메서드 (Template Method)

큰 작업 흐름은 부모가 정하고 세부동작은 자식이 구현하는 패턴이다.
작업 순서는 고정하고 일부 내용만 자식 클래스마다 다르게 만드는 것이라고 이해하면 된다.

템플릿 메서드 예시 (1)

나쁜 예시

// 데이터베이스로부터 사용자 검색을 처리하는 객체
class FindUserQuery{
    void execute() {
        // DB와 통신을 시도
        System.out.println("DB에 통신 시도중");
        System.out.println("DB에 연결되었습니다");

        // 사용자 검색
        System.out.println("<사용자> 데이터 베이스 검색 중...");
        System.out.println("사용자 'John Doe'를 찾았습니다");

        //DB 통신 종료
        System.out.println("DB와 통신을 종료합니다");
    }
}

// 데이터베이스로부터 제품 검색을 처리하는 객체
class FindProductQuery{
    void execute() {
        // DB와 통신을 시도
        System.out.println("DB에 연결 시도중");
        System.out.println("DB에 연결되었습니다");

        // 사용자 검색
        System.out.println("<제품> 데이터 베이스 검색 중...");
        System.out.println("상품 '레고'를 찾았습니다");

        //DB 통신 종료
        System.out.println("DB와 연결을 종료합니다");
    }
}

public class Main {
    public static void main(String[] args) {
        // 사용자 검색 요청
        FindUserQuery userQuery = new FindUserQuery();
        userQuery.execute();

        // 제품 검색 요청
        FindProductQuery productQuery = new FindProductQuery();
        productQuery.execute();
    }
    
}

이 코드는 중복 코드가 너무 많고 템플릿 메서드 패턴이 필요한 나쁜 예시라고 볼 수 있다.

현재 FindUserQuery, FindProductQuery 두 클래스 모두 DB연결, DB종료 같은 흐름이 거의 똑같다.
공통 로직이 중복되고 있는 것이다.
나중에 DB 연결 방식이 바뀌면 FindUserQuery, FindProductQuery 둘 다 수정해야 한다는 점이다.
즉 유지보수가 어려워진다.

또 전체 흐름도 거의 동일하다.
현재 흐름은 DB 연결 -> 검색 수행 -> DB 종료로 고정되어있다.
하지만 검색 대상만 다르다.
즉 변하는 부분은 검색 로직뿐이라는 것이다.

그렇다면 좋은 구조는 어떻게 써야할까?

좋은 예시

// 데이터베이스 검색의 뼈대(Template)를 정의하는 추상 클래스
abstract class DbQueryTemplate {
    // 템플릿 메서드: 실행 흐름을 정의합니다
    // final이 붙은 이유: 자식에서 수정하지 못하도록 한다
    public final void execute() {
        //DB와 통신을 시도합니다
        System.out.println("DB에 통신 시도중");
        System.out.println("DB에 연결되었습니다");

        // 자식이 구현할 부분
        runBusiness();

        // DB 통신 종료
        System.out.println("DB와 통신을 종료합니다");
        System.out.println("exit");
    }

    // 추상 메서드
    abstract void runBusiness();
}

// 사용자 검색을 처리하는 객체
class FindUserQuery extends DbQueryTemplate {
    void runBusiness() {
        System.out.println("<사용자> 데이터베이스 검색중..");
        System.out.println("사용자 'John Doe'를 찾았습니다");
    }
}

// 제품 검색을 처리하는 객체
class FindProductQuery extends DbQueryTemplate {
    // 추상 메서드를 구현합니다
    void runBusiness() {
        System.out.println("<상품> 데이터베이스 검색중..");
        System.out.println("상품 '레고'를 찾았습니다");
    }
}

public class Main {
    public static void main(String[] args) {
        // 1 사용자 조회하기
        DbQueryTemplate userQuery = new FindUserQuery();
        userQuery.execute();

        // 2 상품 조회하기
        DbQueryTemplate productQuery = new FindProductQuery();
        productQuery.execute();
    }
}

이 코드는 템플릿 메서드 패턴을 잘 적용한 좋은 예시라고 볼 수 있다.
이전 나쁜 예시에서는 사용자 검색, 상품 검색 클래스마다 DB연결, DB종료 코드가 계속 중복되고 있었다.
하지만 이번 코드에서는 공통흐름을 DbQueryTemplate 라는 부모 클래스에 모아두었다.
특히 public final void execute() 메서드가 핵심이다.
이 메서드가 DB연결, 비즈니스 실행 DB종료라는 전체 흐름을 관리한다.
즉 작업 순서(템플릿)을 부모가 관리하는 구조가 된 것이다.

그리고 실제로 달라지는 부분만 runBusiness() 추상 메서드로 분리했다.
사용자 검색 방식, 상품 검색 방식같이 변하는 부분만 자식 클래스가 구현한다.
즉 공통 부분과 변경 부분이 깔끔하게 분리된 것이다.

또 좋은 점은 DB 연결 방식이 바뀌어도 DbQueryTemplate만 수정하면 된다.
즉 유지보수가 매우 쉬워진다.

그리고 final이 붙어 있는 것도 중요하다.
execute() 메서드를 자식이 함부로 수정하지 못하게 막아서 전체 실행 흐름을 보호하고 있는 것이다.

템플릿 메서드 예시 (2)

나쁜 예시

class PotatoChipRecipe {
    void perform() {
        System.out.println("기름을 준비합니다");
        System.out.println("기름을 끓입니다");

        System.out.println("감자를 썰어서 기름에 넣어줍니다");

        System.out.println("튀긴 칩을 접시에 올려놓습니다");
        System.out.println("기름을 정리합니다");
        System.out.println("요리 끝!");
    }
}

class SweetPotatoCipRecipe {
    void perform() {
        System.out.println("기름을 준비합니다");
        System.out.println("기름을 끓입니다");

        System.out.println("고구마를 썰어서 기름에 넣어줍니다");

        System.out.println("튀긴 칩을 접시에 올려놓습니다");
        System.out.println("기름을 정리합니다");
        System.out.println("요리 끝!");
    }
}

class CornChipRecipe {
    void perfor() {
        System.out.println("기름을 준비합니다");
        System.out.println("기름을 끓입니다");

        System.out.println("옥수수 반죽을 만듭니다");
        System.out.println("반죽을 적당한 크기로 잘라 기름에 넣어줍니다");

        System.out.println("튀긴 칩을 접시에 올려놓습니다");
        System.out.println("기름을 정리합니다");
        System.out.println("요리 끝!");
    }
}

public class Main {
    public static void main(String[] args) {
        
        PotatoChipRecipe potatoChipRecipe = new PotatoChipRecipe();
        potatoChipRecipe.perform();

        SweetPotatoCipRecipe sweetPotatoCipRecipe = new SweetPotatoCipRecipe();
        sweetPotatoCipRecipe.perform();

        CornChipRecipe cornChipRecipe = new CornChipRecipe();
        cornChipRecipe.perfor();
    }
    
}

이 코드 또한 공통 코드가 계속 중복되는 나쁜 구조이다.
공통 로직이 여러 클래스에 반복되고 있다라는 것이다.
그래서 공통 흐름은 부모 클래스에 모으고 변하는 요리 부분만 자식이 구현하면 된다.
좋은 코드를 살펴보자.

좋은 예시

abstract class ChipRecipeTemplate {
    final void perform() {
        System.out.println("기름을 준비합니다");
        System.out.println("기름을 끓입니다");

        runBusiness();

        System.out.println("튀긴 칩을 접시에 올려놓습니다");
        System.out.println("기름을 정리합니다");
        System.out.println("요리 끝!");
    }

    abstract void runBusiness();
}

class PotatoChipRecipe extends ChipRecipeTemplate {
    void runBusiness() {
        System.out.println("감자를 썰어서 기름에 넣어줍니다");
    }
}

class sweetPotatoChipRecipe extends ChipRecipeTemplate {
    void runBusiness() {
        System.out.println("고구마를 썰어서 기름에 넣어줍니다");
    }
}

class CornChipRecipe extends ChipRecipeTemplate {
    void runBusiness() {
        System.out.println("옥수수를 기름에 넣어줍니다");
    }
}

public class Main {
    public static void main(String[] args) {

        // 감자
        ChipRecipeTemplate potatoChipRecipe = new PotatoChipRecipe();
        potatoChipRecipe.perform();

        // 고구마
        ChipRecipeTemplate sweetPotatoChipRecipe = new sweetPotatoChipRecipe();
        sweetPotatoChipRecipe.perform();

        //옥수수
        ChipRecipeTemplate cornChipRecipe = new CornChipRecipe();
        cornChipRecipe.perform();
    }
}

이 코드는 템플릿 메서드 패턴으로 공통 코드를 제거한 좋은 구조이다.
기름 준비, 기름 끓이기, 접시에 올리기, 정리 같은 공통 흐름을 ChipRecipeTemplate 부모 클래스가 관리하고 있다.
그리고 자식 클래스는 감자, 고구마, 옥수수처럼 달라지는 요리 부분만 구현하고 있다.
즉 공통 부분과 변경 부분이 분리가 된 구조인 것이다.
덕분에 중복 코드가 감소하고 확장이 편리하며 유지보수가 쉬워지게 되었다.
즉 공통 흐름은 부모, 변하는 부분은 자식이라는 템플릿 메서드 패턴을 잘 적용한 좋은 코드라고 볼 수 있다.


싱글턴 (Singleton)

싱글턴은 객체를 단 하나만 생성하도록 제한하는 패턴이다.
즉 프로그램 전체에서 인스턴스 하나만 사용하게 만드는 것이다.
주로 스프링 서버의 빈(Bean)들이 싱글턴이다.

// 싱글턴 생성을 위한 클래스
class MySingleton {

    // 정적멤버 변수에 싱글턴 인스턴스를 저장한다
    private static final MySingleton mySingleton = new MySingleton();

    // 외부에서 인스턴스 생성불가
    private MySingleton() {}

    // 싱글턴을 반환하는 정적 메서드
    public static MySingleton getInstance() {
        return mySingleton;
    }
}

public class Main {
    public static void main(String[] args) {
        MySingleton singleton1 = MySingleton.getInstance();
        MySingleton singleton2 = MySingleton.getInstance();

        // 객체에서 동등 연선자를 사용하는 경우
        // 두 객체가 동일한지 비교
        // 객체가 동일하다 = 메모리 상에서 두 변수가 같은 곳을 가리킨다
        System.out.println(singleton1 == singleton2);
    }
}

이 코드는 싱글턴 패턴을 사용해서 객체를 하나만 생성하고 공유하는 코드이다.
핵심은 private static final Mysingleton mySingleton 부분이다.
여기서 프로그램이 실행될 때 mySingleton 객체가 딱 한 번 생성된다.
그리고 private MySingleton() {} 을 통해 외부에서 new로 객체 생성을 금지하고 있다.
즉 new MySingleton()을 밖에서는 사용할 수 없다
그래서 객체를 사용할 때는 getInstance() 메서드를 호출한다.

MySingleton singleton1 = MySingleton.getInstance(); 이 코드는 이미 만들어진 싱글턴 객체를 받아오는 것이다.

그리고 singleton1 == singleton2는 두 변수가 같은 객체를 가리키는지 비교한다.
현재는 둘 다 같은 싱글턴 객체를 공유하므로 true가 출력된다.

즉 이 코드의 핵심은 객체를 하나만 생성하고 모든 곳에서 같은 객체를 공유해서 사용하는 싱글턴 패턴이라는 점이다.


마무리

오늘은 템플릿 메서드 패턴과 싱글턴 패턴에 대해 학습하며 객체지향 설계가 왜 중요한지 다시 한 번 느낄 수 있었다.

특히 템플릿 메서드 패턴은 단순히 코드를 줄이는 것이 아니라, 공통 흐름과 변경되는 부분을 분리해서 유지보수를 쉽게 만든다는 점이 인상 깊었다.
처음에는 단순히 “중복 제거” 정도로만 생각했지만, 실제로는 프로그램의 구조를 더 안정적으로 만드는 설계 방식이라는 것을 이해하게 되었다.

또 싱글턴 패턴을 배우면서 객체를 하나만 생성해서 공유하는 방식과, 왜 생성자를 private으로 막고 getInstance()로 객체를 반환하는지에 대해서도 이해할 수 있었다.
처음에는 “왜 굳이 반환하지?”라는 생각이 들었지만, 결국 이미 생성된 객체를 모든 곳에서 공유하기 위해 필요한 구조라는 점이 흥미로웠다.

오늘은 단순히 코드를 작성하는 것보다, “왜 이렇게 설계해야 하는가?”를 많이 고민해본 시간이었다.
점점 객체지향의 흐름이 연결되어가는 느낌이 들어서 재미있었고, 앞으로 배우게 될 디자인 패턴들도 기대된다.

0개의 댓글