템플릿 메서드 패턴(Template Method Pattern)
템플릿 메서드 패턴(Template Method Pattern)은 객체 지향 설계 패턴 중 하나로, 상위 클래스에서 알고리즘의 구조를 정의하고, 하위 클래스에서 알고리즘의 특정 단계를 구현하는 패턴입니다. 이 패턴은 알고리즘의 뼈대를 상위 클래스에서 정의하고, 하위 클래스에서 세부적인 동작을 재정의하거나 확장할 수 있도록 설계되어 있습니다.
템플릿 메서드 패턴의 구성 요소
- AbstractClass(추상 클래스):
- 알고리즘의 구조를 정의하는 추상 클래스입니다.
- 전체적인 알고리즘 흐름을 구현한 템플릿 메서드를 포함하고 있습니다.
- 알고리즘의 일부 단계는 추상 메서드로 정의되며, 이 메서드들은 하위 클래스에서 구체적인 구현을 제공합니다.
- ConcreteClass(구체 클래스):
- AbstractClass에서 정의된 추상 메서드를 구현하여, 알고리즘의 구체적인 단계를 정의합니다.
- 상위 클래스에서 제공하는 템플릿 메서드를 그대로 사용할 수 있으며, 필요에 따라 일부 단계를 재정의할 수 있습니다.
템플릿 메서드 패턴의 구성 요소
- AbstractClass(추상 클래스):
- 알고리즘의 구조를 정의하는 추상 클래스입니다.
- 전체적인 알고리즘 흐름을 구현한 템플릿 메서드를 포함하고 있습니다.
- 알고리즘의 일부 단계는 추상 메서드로 정의되며, 이 메서드들은 하위 클래스에서 구체적인 구현을 제공합니다.
- 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(); // 체스 게임 초기화 -> 체스 게임 시작 -> 체스 게임 종료
}
}
템플릿 메서드 패턴의 장점
- 코드 재사용성:
- 공통된 알고리즘 구조를 상위 클래스에서 정의함으로써 코드의 중복을 줄이고, 재사용성을 높일 수 있습니다.
- 유연성:
- 알고리즘의 특정 단계들을 하위 클래스에서 재정의하여, 다양한 요구 사항에 맞게 유연하게 확장할 수 있습니다.
- 알고리즘의 유지보수성:
- 알고리즘의 구조가 한 곳에서 정의되기 때문에, 알고리즘 변경 시 코드 수정이 쉽고 유지보수가 용이합니다.
템플릿 메서드 패턴의 단점
- 하위 클래스에 의존적:
- 템플릿 메서드 패턴은 하위 클래스가 알고리즘의 각 단계를 구현해야 하므로, 하위 클래스의 구현이 제대로 이루어지지 않으면 전체 알고리즘이 의도한 대로 동작하지 않을 수 있습니다.
- 알고리즘의 변경이 어려움:
- 알고리즘의 주요 구조가 상위 클래스에 고정되어 있어, 알고리즘의 구조를 변경해야 하는 경우 패턴을 수정하기 어렵습니다.
콜백(Callback)
콜백(Callback)은 프로그래밍에서 다른 코드가 호출할 수 있도록 미리 정의된 함수나 메서드를 의미합니다. 즉, 어떤 작업이 완료되거나 특정 이벤트가 발생했을 때 자동으로 호출되는 함수입니다. 콜백은 비동기 처리, 이벤트 처리, 또는 함수의 재사용성을 높이기 위해 자주 사용됩니다.
콜백의 주요 특징
- 함수 포인터 또는 함수 객체:
- 콜백은 함수를 다른 함수의 인자로 전달할 수 있도록 합니다. 자바에서는 주로 인터페이스나 람다 표현식을 사용하여 콜백을 구현합니다.
- 비동기 처리:
- 비동기 작업(예: 파일 읽기, 네트워크 요청 등)이 완료된 후 콜백 함수가 호출됩니다. 이를 통해 비동기 작업이 끝났을 때의 동작을 정의할 수 있습니다.
- 이벤트 처리:
- 사용자 인터페이스(UI) 프로그래밍에서 버튼 클릭, 마우스 이동 등의 이벤트가 발생했을 때 특정 동작을 수행하는 콜백 함수를 설정할 수 있습니다.
- 유연성:
- 콜백을 사용하면 함수나 메서드의 동작을 동적으로 변경할 수 있습니다. 따라서 코드의 유연성과 재사용성이 높아집니다.
자바에서의 콜백 예시
자바에서는 콜백을 주로 인터페이스를 통해 구현합니다. 인터페이스를 사용하여 특정 동작을 정의하고, 이 인터페이스를 구현하는 클래스에서 그 동작을 구체화합니다.
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 쿼리 결과를 자바 객체로 쉽게 변환할 수 있으며, 코드의 재사용성과 가독성을 높이는 데 기여합니다.
주요 특징
- ResultSet을 객체로 매핑:
- RowMapper는 데이터베이스 쿼리의 결과로 반환된 ResultSet의 각 행을 하나의 자바 객체로 변환하는 역할을 합니다.
- 예를 들어, 데이터베이스의 각 행을 특정 클래스의 인스턴스로 변환할 수 있습니다.
- 재사용성:
- RowMapper를 사용하면 여러 쿼리에서 동일한 매핑 로직을 재사용할 수 있습니다. 이를 통해 코드 중복을 줄이고 유지 보수를 용이하게 합니다.
- 템플릿 메서드 패턴:
- 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.
- Stream은 두 가지 유형의 연산을 지원합니다:
- 지연 연산(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을 상속받아, 키와 값을 모두 문자열로 저장합니다.
프로그램 실행시 필요한 정보들을 저장하는 것
주요 특징
- 키-값 쌍으로 구성:
- Properties 클래스는 설정 정보를 키-값 쌍으로 관리합니다. 키와 값은 모두 String 타입이어야 하며, 설정 파일에서 주로 텍스트 기반으로 저장됩니다.
- 설정 파일 관리:
- .properties 파일 형태로 설정 정보를 외부 파일에 저장할 수 있으며, 이 파일을 통해 애플리케이션의 동작을 쉽게 변경할 수 있습니다.
- 예를 들어, 데이터베이스 설정, 애플리케이션 환경 변수 등을 관리할 수 있습니다.
- 파일에서 읽기 및 쓰기:
- Properties 클래스는 설정 파일을 읽어와 메모리에 로드하거나, 메모리에 있는 설정 정보를 파일로 저장하는 기능을 제공합니다.
- 기본 값 지원:
- 키에 대한 값을 요청할 때, 해당 키가 없을 경우 기본값을 반환하도록 설정할 수 있습니다.
주요 메서드
- 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