본문 바로가기
Everyday Study

2024.08.12(월) { 템플릿 메서드 패턴, 콜백, RowMapper, Stream, Properties }

by xogns93 2024. 8. 12.

템플릿 메서드 패턴(Template Method Pattern)

 

템플릿 메서드 패턴(Template Method Pattern)은 객체 지향 설계 패턴 중 하나로, 상위 클래스에서 알고리즘의 구조를 정의하고, 하위 클래스에서 알고리즘의 특정 단계를 구현하는 패턴입니다. 이 패턴은 알고리즘의 뼈대를 상위 클래스에서 정의하고, 하위 클래스에서 세부적인 동작을 재정의하거나 확장할 수 있도록 설계되어 있습니다.

템플릿 메서드 패턴의 구성 요소

  1. AbstractClass(추상 클래스):
    • 알고리즘의 구조를 정의하는 추상 클래스입니다.
    • 전체적인 알고리즘 흐름을 구현한 템플릿 메서드를 포함하고 있습니다.
    • 알고리즘의 일부 단계는 추상 메서드로 정의되며, 이 메서드들은 하위 클래스에서 구체적인 구현을 제공합니다.
  2. ConcreteClass(구체 클래스):
    • AbstractClass에서 정의된 추상 메서드를 구현하여, 알고리즘의 구체적인 단계를 정의합니다.
    • 상위 클래스에서 제공하는 템플릿 메서드를 그대로 사용할 수 있으며, 필요에 따라 일부 단계를 재정의할 수 있습니다.

템플릿 메서드 패턴의 구성 요소

  1. AbstractClass(추상 클래스):
    • 알고리즘의 구조를 정의하는 추상 클래스입니다.
    • 전체적인 알고리즘 흐름을 구현한 템플릿 메서드를 포함하고 있습니다.
    • 알고리즘의 일부 단계는 추상 메서드로 정의되며, 이 메서드들은 하위 클래스에서 구체적인 구현을 제공합니다.
  2. ConcreteClass(구체 클래스):
    • AbstractClass에서 정의된 추상 메서드를 구현하여, 알고리즘의 구체적인 단계를 정의합니다.
    • 상위 클래스에서 제공하는 템플릿 메서드를 그대로 사용할 수 있으며, 필요에 따라 일부 단계를 재정의할 수 있습니다.
// 추상 클래스
abstract class Game {
    // 템플릿 메서드
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }

    // 알고리즘의 각 단계를 정의하는 추상 메서드
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();
}

// 구체 클래스 1
class Soccer extends Game {
    @Override
    void initialize() {
        System.out.println("축구 게임 초기화.");
    }

    @Override
    void startPlay() {
        System.out.println("축구 게임 시작.");
    }

    @Override
    void endPlay() {
        System.out.println("축구 게임 종료.");
    }
}

// 구체 클래스 2
class Chess extends Game {
    @Override
    void initialize() {
        System.out.println("체스 게임 초기화.");
    }

    @Override
    void startPlay() {
        System.out.println("체스 게임 시작.");
    }

    @Override
    void endPlay() {
        System.out.println("체스 게임 종료.");
    }
}

 

템플릿 메서드 패턴 사용 예시

public class TemplateMethodPatternExample {
    public static void main(String[] args) {
        Game game = new Soccer();
        game.play(); // 축구 게임 초기화 -> 축구 게임 시작 -> 축구 게임 종료

        System.out.println();

        game = new Chess();
        game.play(); // 체스 게임 초기화 -> 체스 게임 시작 -> 체스 게임 종료
    }
}

템플릿 메서드 패턴의 장점

  1. 코드 재사용성:
    • 공통된 알고리즘 구조를 상위 클래스에서 정의함으로써 코드의 중복을 줄이고, 재사용성을 높일 수 있습니다.
  2. 유연성:
    • 알고리즘의 특정 단계들을 하위 클래스에서 재정의하여, 다양한 요구 사항에 맞게 유연하게 확장할 수 있습니다.
  3. 알고리즘의 유지보수성:
    • 알고리즘의 구조가 한 곳에서 정의되기 때문에, 알고리즘 변경 시 코드 수정이 쉽고 유지보수가 용이합니다.

템플릿 메서드 패턴의 단점

  1. 하위 클래스에 의존적:
    • 템플릿 메서드 패턴은 하위 클래스가 알고리즘의 각 단계를 구현해야 하므로, 하위 클래스의 구현이 제대로 이루어지지 않으면 전체 알고리즘이 의도한 대로 동작하지 않을 수 있습니다.
  2. 알고리즘의 변경이 어려움:
    • 알고리즘의 주요 구조가 상위 클래스에 고정되어 있어, 알고리즘의 구조를 변경해야 하는 경우 패턴을 수정하기 어렵습니다.

콜백(Callback)

 

콜백(Callback)은 프로그래밍에서 다른 코드가 호출할 수 있도록 미리 정의된 함수나 메서드를 의미합니다. 즉, 어떤 작업이 완료되거나 특정 이벤트가 발생했을 때 자동으로 호출되는 함수입니다. 콜백은 비동기 처리, 이벤트 처리, 또는 함수의 재사용성을 높이기 위해 자주 사용됩니다.

 

콜백의 주요 특징

  1. 함수 포인터 또는 함수 객체:
    • 콜백은 함수를 다른 함수의 인자로 전달할 수 있도록 합니다. 자바에서는 주로 인터페이스나 람다 표현식을 사용하여 콜백을 구현합니다.
  2. 비동기 처리:
    • 비동기 작업(예: 파일 읽기, 네트워크 요청 등)이 완료된 후 콜백 함수가 호출됩니다. 이를 통해 비동기 작업이 끝났을 때의 동작을 정의할 수 있습니다.
  3. 이벤트 처리:
    • 사용자 인터페이스(UI) 프로그래밍에서 버튼 클릭, 마우스 이동 등의 이벤트가 발생했을 때 특정 동작을 수행하는 콜백 함수를 설정할 수 있습니다.
  4. 유연성:
    • 콜백을 사용하면 함수나 메서드의 동작을 동적으로 변경할 수 있습니다. 따라서 코드의 유연성과 재사용성이 높아집니다.

자바에서의 콜백 예시

자바에서는 콜백을 주로 인터페이스를 통해 구현합니다. 인터페이스를 사용하여 특정 동작을 정의하고, 이 인터페이스를 구현하는 클래스에서 그 동작을 구체화합니다.

 

1. 인터페이스를 이용한 콜백

// 콜백 인터페이스 정의
interface Callback {
    void onComplete(String message);
}

// 작업을 수행하는 클래스
class Worker {
    // 콜백을 전달받는 메서드
    public void doWork(Callback callback) {
        // 작업 수행
        System.out.println("작업을 수행 중입니다...");

        // 작업 완료 후 콜백 호출
        callback.onComplete("작업이 완료되었습니다!");
    }
}

// 콜백을 구현하는 클래스
class MyCallback implements Callback {
    @Override
    public void onComplete(String message) {
        System.out.println("콜백 받음: " + message);
    }
}

 

2. 람다를 이용한 콜백

자바 8 이상에서는 람다 표현식을 사용하여 콜백을 더 간단하게 구현할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();
        
        // 람다 표현식을 사용한 콜백 구현
        worker.doWork(message -> {
            System.out.println("람다 콜백 받음: " + message);
        });
    }
}

 

출력 예시 :

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();

        // MyCallback 클래스를 사용한 콜백
        MyCallback myCallback = new MyCallback();
        worker.doWork(myCallback);
        // 출력: 작업을 수행 중입니다...
        //       콜백 받음: 작업이 완료되었습니다!

        // 람다 표현식을 사용한 콜백
        worker.doWork(message -> {
            System.out.println("람다 콜백 받음: " + message);
        });
        // 출력: 작업을 수행 중입니다...
        //       람다 콜백 받음: 작업이 완료되었습니다!
    }
}

RowMapper

 

RowMapper는 Java의 JDBC 템플릿에서 사용되는 인터페이스로, 데이터베이스로부터 조회된 결과 집합(ResultSet)의 각 행(row)을 매핑하여 원하는 형태의 객체로 변환해주는 역할을 합니다. 이를 통해 SQL 쿼리 결과를 자바 객체로 쉽게 변환할 수 있으며, 코드의 재사용성과 가독성을 높이는 데 기여합니다. 

 

주요 특징

  1. ResultSet을 객체로 매핑:
    • RowMapper는 데이터베이스 쿼리의 결과로 반환된 ResultSet의 각 행을 하나의 자바 객체로 변환하는 역할을 합니다.
    • 예를 들어, 데이터베이스의 각 행을 특정 클래스의 인스턴스로 변환할 수 있습니다.
  2. 재사용성:
    • RowMapper를 사용하면 여러 쿼리에서 동일한 매핑 로직을 재사용할 수 있습니다. 이를 통해 코드 중복을 줄이고 유지 보수를 용이하게 합니다.
  3. 템플릿 메서드 패턴:
    • RowMapper는 템플릿 메서드 패턴을 적용한 인터페이스입니다. mapRow 메서드를 구현하여 매핑 로직을 정의하면, JdbcTemplate은 내부적으로 이 메서드를 호출하여 결과를 객체로 변환합니다.

RowMapper 예시

아래는 RowMapper를 사용하여 데이터베이스의 사용자 테이블에서 사용자 정보를 조회하고, 이를 User 객체로 매핑하는 예시입니다.

// User 클래스: 데이터베이스의 사용자 정보를 담기 위한 클래스
public class User {
    private int id;
    private String name;
    private String email;

    // 생성자, getter, setter 생략

    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
    }
}

// RowMapper 구현: ResultSet의 각 행을 User 객체로 매핑
public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setName(rs.getString("name"));
        user.setEmail(rs.getString("email"));
        return user;
    }
}

// JdbcTemplate을 사용한 데이터베이스 조회 예시
public class UserDao {
    private JdbcTemplate jdbcTemplate;

    // jdbcTemplate 주입을 위한 생성자
    public UserDao(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // 모든 사용자를 조회하는 메서드
    public List<User> getAllUsers() {
        String sql = "SELECT id, name, email FROM users";
        return jdbcTemplate.query(sql, new UserRowMapper());
    }
}

// Main 클래스에서의 실행 예시
public class Main {
    public static void main(String[] args) {
        // 데이터베이스 연결 설정은 생략 (DataSource 생성)
        
        UserDao userDao = new UserDao(dataSource);

        // 모든 사용자 정보 조회
        List<User> users = userDao.getAllUsers();
        for (User user : users) {
            System.out.println(user);
        }
    }
}

Stream 

 

Java의 Stream API는 Java 8에서 도입된 기능으로, 컬렉션(예: List, Set) 또는 배열 등의 데이터 소스를 처리하는 데 사용됩니다. Stream API는 함수형 프로그래밍의 개념을 도입하여, 데이터 처리를 간결하고 효율적으로 수행할 수 있게 해줍니다. Stream은 데이터의 흐름을 추상화하여 선언적 방식으로 데이터를 처리할 수 있도록 합니다.

 

Stream의 주요 특징

  • 선언형 프로그래밍:
    • Stream API를 사용하면 데이터 처리 로직을 선언적으로 표현할 수 있습니다. 즉, 어떻게 처리할지보다는 무엇을 처리할지를 더 명확히 나타낼 수 있습니다.
    • 예를 들어, 리스트에서 짝수만 필터링하고 각 요소를 제곱하는 작업을 할 때, 명령형 프로그래밍에서는 반복문을 사용해야 하지만, Stream을 사용하면 간단한 메서드 체이닝으로 표현할 수 있습니다.
  • 중간 연산과 최종 연산:
    • Stream은 두 가지 유형의 연산을 지원합니다:
      • 중간 연산: 스트림을 반환하는 연산으로, 여러 중간 연산을 연결하여(메서드 체이닝) 파이프라인을 구성할 수 있습니다. 예: filter, map, sorted.
      • 최종 연산: 스트림을 소비하고 결과를 반환하거나 다른 연산을 수행하는 연산으로, 스트림이 소모되고 더 이상 사용되지 않습니다. 예: collect, forEach, reduce.
  • 지연 연산(Lazy Evaluation):
    • 중간 연산은 지연(lazy) 실행됩니다. 이는 최종 연산이 호출될 때까지 실제로 실행되지 않음을 의미합니다. 이러한 특성 덕분에 불필요한 연산을 최소화하고 성능을 최적화할 수 있습니다
  • 병렬 처리 지원
    • Stream API는 기본적으로 순차적으로 실행
  • 불변성(Immutability):
    • Stream 자체는 불변(immutable)하며, 원본 데이터를 변경하지 않습니다. 스트림 연산은 항상 새로운 스트림을 생성하고, 원본 컬렉션 또는 배열에는 영향을 미치지 않습니다.
  • 파이프라인 처리:
    • Stream은 여러 중간 연산을 통해 데이터를 변환하고 최종 연산에서 결과를 얻는 파이프라인 구조를 갖습니다. 이 구조는 데이터 처리의 각 단계를 명확하게 분리할 수 있게 해줍니다.
  • 재사용 불가(Non-Reusable):
    • 한 번 사용된 스트림은 재사용할 수 없습니다. 최종 연산을 통해 스트림이 소비된 후에는 해당 스트림을 다시 사용할 수 없으며, 필요시 새로운 스트림을 생성해야 합니다.

Stream 예시

아래는 Stream API를 사용하여 리스트에서 짝수만 필터링하고, 각 요소를 제곱한 후, 결과를 리스트로 수집하는 간단한 예시입니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> evenSquares = numbers.stream()         // 스트림 생성
                                            .filter(n -> n % 2 == 0) // 짝수 필터링
                                            .map(n -> n * n)         // 각 요소를 제곱
                                            .collect(Collectors.toList()); // 결과를 리스트로 수집

        System.out.println(evenSquares);  // 출력: [4, 16, 36, 64, 100]
    }
}

Properties

 

Java의 Properties 클래스는 키-값 쌍으로 구성된 설정 정보를 관리하는 데 사용됩니다. 주로 애플리케이션의 설정 정보를 외부 파일에 저장하고, 이를 쉽게 읽고 쓸 수 있도록 도와줍니다. Properties 클래스는 Hashtable을 상속받아, 키와 값을 모두 문자열로 저장합니다.

프로그램 실행시 필요한 정보들을 저장하는 것

주요 특징

  1. 키-값 쌍으로 구성:
    • Properties 클래스는 설정 정보를 키-값 쌍으로 관리합니다. 키와 값은 모두 String 타입이어야 하며, 설정 파일에서 주로 텍스트 기반으로 저장됩니다.
  2. 설정 파일 관리:
    • .properties 파일 형태로 설정 정보를 외부 파일에 저장할 수 있으며, 이 파일을 통해 애플리케이션의 동작을 쉽게 변경할 수 있습니다.
    • 예를 들어, 데이터베이스 설정, 애플리케이션 환경 변수 등을 관리할 수 있습니다.
  3. 파일에서 읽기 및 쓰기:
    • Properties 클래스는 설정 파일을 읽어와 메모리에 로드하거나, 메모리에 있는 설정 정보를 파일로 저장하는 기능을 제공합니다.
  4. 기본 값 지원:
    • 키에 대한 값을 요청할 때, 해당 키가 없을 경우 기본값을 반환하도록 설정할 수 있습니다.

주요 메서드

  • load(InputStream inStream): 입력 스트림을 통해 .properties 파일을 읽어들여 Properties 객체를 초기화합니다.
  • store(OutputStream out, String comments): 현재 Properties 객체의 내용을 출력 스트림으로 내보내어 파일로 저장합니다.
  • getProperty(String key): 지정된 키에 대한 값을 반환합니다. 만약 키가 없으면 null을 반환합니다.
  • getProperty(String key, String defaultValue): 지정된 키에 대한 값을 반환하되, 키가 없으면 지정된 기본값을 반환합니다.
  • setProperty(String key, String value): 지정된 키에 대한 값을 설정합니다.

Properties 클래스의 사용 예시

1. Properties 파일 읽기

아래는 config.properties 파일을 읽어서 설정 정보를 출력하는 간단한 예시입니다.

 

 

config.properties 파일 예시:

database.url=jdbc:mysql://localhost:3306/mydb
database.user=root
database.password=password
app.name=MyApp
app.version=1.0.0

Java 코드:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class PropertiesExample {
    public static void main(String[] args) {
        Properties properties = new Properties();
        try (InputStream input = new FileInputStream("config.properties")) {
            // properties 파일 읽기
            properties.load(input);

            // 속성 출력
            String dbUrl = properties.getProperty("database.url");
            String dbUser = properties.getProperty("database.user");
            String dbPassword = properties.getProperty("database.password");
            String appName = properties.getProperty("app.name");
            String appVersion = properties.getProperty("app.version");

            System.out.println("Database URL: " + dbUrl);
            System.out.println("Database User: " + dbUser);
            System.out.println("Database Password: " + dbPassword);
            System.out.println("Application Name: " + appName);
            System.out.println("Application Version: " + appVersion);

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

 

출력 결과:

Database URL: jdbc:mysql://localhost:3306/mydb
Database User: root
Database Password: password
Application Name: MyApp
Application Version: 1.0.0