| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
- 플러스 백엔드
- 알고리즘
- 삽입
- Spring
- jdk
- code blocks
- redis
- stack
- JPA
- Gradle
- 티스토리챌린지
- EDA
- event
- Unity
- docker
- 이진트리
- bean
- Kafka
- Kotlin
- Java
- 연습문제
- 백준
- 코딩테스트
- 프로그래머스
- 탐색
- 오블완
- MSA
- 아키텍처
- 트리
- jre
- Today
- Total
Repository
7단계: 디자인 패턴 & 아키텍처 본문
7단계: 디자인 패턴 & 아키텍처
4년간의 실무에서 마주한 복잡한 설계 문제들을 해결한 경험을 바탕으로, 단순한 패턴 암기가 아닌 '언제', '왜', '어떻게' 사용해야 하는지를 실전 관점에서 다룹니다.
7.1 생성 패턴
싱글톤 패턴: 올바른 구현 방법
다양한 싱글톤 구현 방식과 함정들
실무에서 마주한 싱글톤 문제들
// ❌ 문제 1: Eager Initialization의 문제점
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
System.out.println("EagerSingleton created");
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
// 문제점:
// 1. 클래스 로딩 시점에 인스턴스 생성 (메모리 낭비)
// 2. 예외 처리가 어려움
// 3. 초기화 순서 문제 발생 가능
Lazy Initialization의 함정
// ❌ 문제 2: Thread-Unsafe Lazy Initialization
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {}
public static UnsafeLazySingleton getInstance() {
if (instance == null) {
instance = new UnsafeLazySingleton(); // 멀티스레드에서 문제!
}
return instance;
}
}
// 문제점:
// 1. 멀티스레드 환경에서 여러 인스턴스 생성 가능
// 2. Race Condition 발생
Thread-Safe하지만 비효율적인 방법
// ❌ 문제 3: Synchronized Method (성능 저하)
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
// 매번 동기화로 인한 성능 저하
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
// 문제점:
// 1. 매번 동기화로 인한 성능 저하
// 2. 불필요한 락 경합
올바른 싱글톤 구현 방법들
Double-Checked Locking (권장)
// ✅ 방법 1: Double-Checked Locking
public class DoubleCheckedSingleton {
// volatile: 메모리 가시성 보장
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {
System.out.println("DoubleCheckedSingleton created");
}
public static DoubleCheckedSingleton getInstance() {
// 첫 번째 체크 (동기화 없이)
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
// 두 번째 체크 (동기화 블록 내부)
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
// 비즈니스 로직
public void doSomething() {
System.out.println("Doing something...");
}
}
// 장점:
// 1. Thread-Safe
// 2. 성능 최적화 (첫 번째 체크는 동기화 없음)
// 3. 지연 초기화
Bill Pugh Singleton (가장 권장)
// ✅ 방법 2: Bill Pugh Singleton (Static Inner Class)
public class BillPughSingleton {
private BillPughSingleton() {
System.out.println("BillPughSingleton created");
}
// static 내부 클래스: getInstance() 호출 시에만 로드됨
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
// 비즈니스 로직
public void processData(String data) {
System.out.println("Processing: " + data);
}
}
// 장점:
// 1. Thread-Safe (JVM이 보장)
// 2. 지연 초기화
// 3. 성능 우수
// 4. 메모리 효율적
Enum Singleton (가장 안전한 방법)
// ✅ 방법 3: Enum Singleton (가장 안전)
public enum EnumSingleton {
INSTANCE;
// 인스턴스 변수
private String data;
private int counter = 0;
// 메서드
public void doSomething() {
System.out.println("EnumSingleton doing something");
counter++;
}
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public int getCounter() {
return counter;
}
}
// 사용 예제
public class EnumSingletonExample {
public static void main(String[] args) {
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.setData("Hello World");
singleton.doSomething();
// 다른 곳에서도 같은 인스턴스
EnumSingleton another = EnumSingleton.INSTANCE;
System.out.println(another.getData()); // "Hello World"
System.out.println(another.getCounter()); // 1
}
}
// 장점:
// 1. Thread-Safe 자동 보장
// 2. Serialization 안전
// 3. Reflection 공격 방어
// 4. 간결한 코드
// 5. JVM이 싱글톤 보장
실무에서의 싱글톤 활용
로깅 시스템 구현
public class Logger {
private static volatile Logger instance;
private final String logFilePath;
private final PrintWriter writer;
private Logger() {
this.logFilePath = "application.log";
try {
this.writer = new PrintWriter(new FileWriter(logFilePath, true));
} catch (IOException e) {
throw new RuntimeException("Failed to initialize logger", e);
}
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public synchronized void log(String level, String message) {
String logEntry = String.format("[%s] %s: %s",
LocalDateTime.now(),
level,
message
);
writer.println(logEntry);
writer.flush();
// 콘솔에도 출력
System.out.println(logEntry);
}
public void info(String message) {
log("INFO", message);
}
public void error(String message) {
log("ERROR", message);
}
public void warn(String message) {
log("WARN", message);
}
public void close() {
if (writer != null) {
writer.close();
}
}
}
// 사용 예제
public class LoggerExample {
public static void main(String[] args) {
Logger logger = Logger.getInstance();
logger.info("Application started");
logger.warn("This is a warning");
logger.error("An error occurred");
// 어디서든 같은 Logger 인스턴스 사용
Logger anotherLogger = Logger.getInstance();
anotherLogger.info("Same logger instance");
}
}
데이터베이스 연결 풀 관리
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
private final Queue<Connection> availableConnections;
private final Set<Connection> usedConnections;
private final int maxConnections;
private DatabaseConnectionPool(int maxConnections) {
this.maxConnections = maxConnections;
this.availableConnections = new ConcurrentLinkedQueue<>();
this.usedConnections = ConcurrentHashMap.newKeySet();
// 초기 연결 생성
initializeConnections();
}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (DatabaseConnectionPool.class) {
if (instance == null) {
instance = new DatabaseConnectionPool(10); // 기본 10개 연결
}
}
}
return instance;
}
private void initializeConnections() {
for (int i = 0; i < maxConnections; i++) {
try {
// 실제 DB 연결 생성 (예시)
Connection conn = DriverManager.getConnection(
"jdbc:h2:mem:testdb", "sa", "");
availableConnections.offer(conn);
} catch (SQLException e) {
throw new RuntimeException("Failed to create connection", e);
}
}
}
public synchronized Connection getConnection() {
Connection conn = availableConnections.poll();
if (conn != null) {
usedConnections.add(conn);
return conn;
}
throw new RuntimeException("No available connections");
}
public synchronized void returnConnection(Connection conn) {
if (usedConnections.remove(conn)) {
availableConnections.offer(conn);
}
}
public synchronized int getAvailableConnections() {
return availableConnections.size();
}
public synchronized int getUsedConnections() {
return usedConnections.size();
}
}
Spring의 싱글톤과 차이점
Spring Singleton vs Java Singleton
// Java Singleton: JVM 레벨에서 하나의 인스턴스
@Component
public class JavaSingleton {
private static JavaSingleton instance;
public static JavaSingleton getInstance() {
if (instance == null) {
instance = new JavaSingleton();
}
return instance;
}
}
// Spring Singleton: Spring 컨테이너 레벨에서 하나의 인스턴스
@Component
@Scope("singleton") // 기본값
public class SpringSingleton {
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
// 사용 예제
@RestController
public class SingletonController {
@Autowired
private SpringSingleton springSingleton; // Spring이 관리하는 싱글톤
@GetMapping("/test")
public String test() {
// Spring 싱글톤 사용
springSingleton.setData("Spring Singleton Data");
// Java 싱글톤 사용
JavaSingleton javaSingleton = JavaSingleton.getInstance();
return "Both singletons work!";
}
}
// 차이점:
// 1. Java Singleton: JVM 전체에서 하나의 인스턴스
// 2. Spring Singleton: Spring 컨테이너 내에서 하나의 인스턴스
// 3. Spring Singleton: 의존성 주입, AOP 등 Spring 기능 활용 가능
// 4. Spring Singleton: 테스트하기 쉬움 (Mock 주입 가능)
Spring에서 싱글톤 주의사항
@Component
public class StatefulService {
// ❌ 문제: 상태를 가진 싱글톤
private int counter = 0;
public void increment() {
counter++; // 멀티스레드에서 문제 발생 가능
}
public int getCounter() {
return counter;
}
}
@Component
public class StatelessService {
// ✅ 해결: 상태 없는 싱글톤
public int processData(int input) {
// 입력값을 받아서 처리하고 결과 반환
return input * 2;
}
public String formatMessage(String template, Object... args) {
// 템플릿과 인자를 받아서 포맷된 문자열 반환
return String.format(template, args);
}
}
빌더 패턴으로 가독성 높이기
점층적 생성자 패턴의 문제점
실무에서 마주한 생성자 문제들
// ❌ 문제 1: 점층적 생성자 패턴 (Telescoping Constructor)
public class User {
private String name;
private String email;
private int age;
private String address;
private String phone;
private String department;
private String position;
private LocalDate joinDate;
// 기본 생성자
public User(String name) {
this(name, null, 0, null, null, null, null, null);
}
// 이름 + 이메일
public User(String name, String email) {
this(name, email, 0, null, null, null, null, null);
}
// 이름 + 이메일 + 나이
public User(String name, String email, int age) {
this(name, email, age, null, null, null, null, null);
}
// 모든 파라미터
public User(String name, String email, int age, String address,
String phone, String department, String position, LocalDate joinDate) {
this.name = name;
this.email = email;
this.age = age;
this.address = address;
this.phone = phone;
this.department = department;
this.position = position;
this.joinDate = joinDate;
}
// 사용 시 문제점
// User user = new User("홍길동", "hong@email.com", 30, "서울시", "010-1234-5678", "개발팀", "개발자", LocalDate.now());
// 파라미터 순서를 헷갈릴 위험!
}
// 문제점:
// 1. 파라미터 순서를 헷갈리기 쉬움
// 2. 가독성이 떨어짐
// 3. 파라미터가 많아질수록 복잡해짐
// 4. 선택적 파라미터 처리가 어려움
JavaBean 패턴의 문제점
// ❌ 문제 2: JavaBean 패턴
public class UserJavaBean {
private String name;
private String email;
private int age;
private String address;
private String phone;
// 기본 생성자
public UserJavaBean() {}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}
// 사용 시 문제점
// UserJavaBean user = new UserJavaBean();
// user.setName("홍길동");
// user.setEmail("hong@email.com");
// user.setAge(30);
// user.setAddress("서울시");
// user.setPhone("010-1234-5678");
//
// 문제점:
// 1. 객체 생성과 설정이 분리됨
// 2. 불변 객체로 만들 수 없음
// 3. 스레드 안전하지 않음
// 4. 일관성 검증이 어려움
빌더 패턴 구현
기본 빌더 패턴
// ✅ 해결책: Builder 패턴
public class User {
// 필수 필드
private final String name;
private final String email;
// 선택 필드
private final int age;
private final String address;
private final String phone;
private final String department;
private final String position;
private final LocalDate joinDate;
// private 생성자
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
this.department = builder.department;
this.position = builder.position;
this.joinDate = builder.joinDate;
}
// Builder 클래스
public static class Builder {
// 필수 파라미터
private final String name;
private final String email;
// 선택 파라미터 (기본값)
private int age = 0;
private String address = "";
private String phone = "";
private String department = "";
private String position = "";
private LocalDate joinDate = LocalDate.now();
public Builder(String name, String email) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
this.name = name;
this.email = email;
}
public Builder age(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Age must be between 0 and 150");
}
this.age = age;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder department(String department) {
this.department = department;
return this;
}
public Builder position(String position) {
this.position = position;
return this;
}
public Builder joinDate(LocalDate joinDate) {
this.joinDate = joinDate;
return this;
}
public User build() {
// 빌드 전 최종 검증
validateUser();
return new User(this);
}
private void validateUser() {
// 비즈니스 규칙 검증
if (age > 0 && age < 18) {
throw new IllegalArgumentException("User must be at least 18 years old");
}
}
}
// Getters
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
public String getAddress() { return address; }
public String getPhone() { return phone; }
public String getDepartment() { return department; }
public String getPosition() { return position; }
public LocalDate getJoinDate() { return joinDate; }
}
// 사용 예제
public class BuilderExample {
public static void main(String[] args) {
// ✅ 가독성 좋은 사용법
User user = new User.Builder("홍길동", "hong@email.com")
.age(30)
.address("서울시 강남구")
.phone("010-1234-5678")
.department("개발팀")
.position("시니어 개발자")
.joinDate(LocalDate.of(2020, 1, 1))
.build();
System.out.println("User: " + user.getName() + " (" + user.getEmail() + ")");
}
}
고급 빌더 패턴: 상속과 제네릭
// ✅ 고급 빌더 패턴: 상속 지원
public abstract class Person {
protected final String name;
protected final String email;
protected final int age;
protected Person(Builder<?> builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
}
// 재귀적 제네릭 타입을 사용한 Builder
public abstract static class Builder<T extends Builder<T>> {
protected String name;
protected String email;
protected int age;
public T name(String name) {
this.name = name;
return self();
}
public T email(String email) {
this.email = email;
return self();
}
public T age(int age) {
this.age = age;
return self();
}
protected abstract T self();
public abstract Person build();
}
}
// Employee 클래스
public class Employee extends Person {
private final String department;
private final String position;
private final LocalDate joinDate;
private Employee(Builder builder) {
super(builder);
this.department = builder.department;
this.position = builder.position;
this.joinDate = builder.joinDate;
}
public static class Builder extends Person.Builder<Builder> {
private String department = "";
private String position = "";
private LocalDate joinDate = LocalDate.now();
public Builder department(String department) {
this.department = department;
return this;
}
public Builder position(String position) {
this.position = position;
return this;
}
public Builder joinDate(LocalDate joinDate) {
this.joinDate = joinDate;
return this;
}
@Override
protected Builder self() {
return this;
}
@Override
public Employee build() {
return new Employee(this);
}
}
// Getters
public String getDepartment() { return department; }
public String getPosition() { return position; }
public LocalDate getJoinDate() { return joinDate; }
}
// 사용 예제
public class AdvancedBuilderExample {
public static void main(String[] args) {
Employee employee = new Employee.Builder()
.name("김철수")
.email("kim@company.com")
.age(28)
.department("개발팀")
.position("주니어 개발자")
.joinDate(LocalDate.of(2023, 3, 1))
.build();
System.out.println("Employee: " + employee.getName() +
" - " + employee.getDepartment() + " " + employee.getPosition());
}
}
Lombok @Builder 활용
Lombok을 사용한 간편한 빌더
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@Builder
public class Product {
// 필수 필드
private final String name;
private final String category;
// 선택 필드
@Builder.Default
private final BigDecimal price = BigDecimal.ZERO;
@Builder.Default
private final int stock = 0;
@Builder.Default
private final String description = "";
@Builder.Default
private final LocalDateTime createdAt = LocalDateTime.now();
// 커스텀 빌더 메서드
@Builder
public static class ProductBuilder {
// 빌드 전 검증
public Product build() {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Product name is required");
}
if (price.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
return new Product(name, category, price, stock, description, createdAt);
}
}
}
// 사용 예제
public class LombokBuilderExample {
public static void main(String[] args) {
// Lombok @Builder 사용
Product product = Product.builder()
.name("MacBook Pro")
.category("Electronics")
.price(new BigDecimal("2500000"))
.stock(10)
.description("Apple MacBook Pro 16-inch")
.build();
System.out.println(product);
}
}
실무 활용: 설정 객체 빌더
@Builder
@Getter
public class DatabaseConfig {
@Builder.Default
private final String host = "localhost";
@Builder.Default
private final int port = 3306;
@Builder.Default
private final String database = "testdb";
private final String username;
private final String password;
@Builder.Default
private final int maxConnections = 10;
@Builder.Default
private final int connectionTimeout = 30000;
@Builder.Default
private final boolean sslEnabled = false;
// 커스텀 빌더 메서드
@Builder
public static class DatabaseConfigBuilder {
public DatabaseConfig build() {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("Username is required");
}
if (password == null) {
throw new IllegalArgumentException("Password is required");
}
return new DatabaseConfig(host, port, database, username, password,
maxConnections, connectionTimeout, sslEnabled);
}
}
// Connection String 생성
public String getConnectionString() {
return String.format("jdbc:mysql://%s:%d/%s?useSSL=%s&serverTimezone=UTC",
host, port, database, sslEnabled);
}
}
// 사용 예제
public class DatabaseConfigExample {
public static void main(String[] args) {
// 개발 환경 설정
DatabaseConfig devConfig = DatabaseConfig.builder()
.host("localhost")
.port(3306)
.database("devdb")
.username("dev")
.password("dev123")
.maxConnections(5)
.build();
// 프로덕션 환경 설정
DatabaseConfig prodConfig = DatabaseConfig.builder()
.host("prod-server.com")
.port(3306)
.database("proddb")
.username("prod")
.password("secure_password")
.maxConnections(20)
.connectionTimeout(60000)
.sslEnabled(true)
.build();
System.out.println("Dev Connection: " + devConfig.getConnectionString());
System.out.println("Prod Connection: " + prodConfig.getConnectionString());
}
}
7.2 구조 패턴
어댑터와 데코레이터 패턴
어댑터 패턴: 레거시 코드 통합
실무에서 마주한 어댑터 패턴 케이스
// ❌ 문제: 서로 다른 인터페이스를 가진 클래스들
// 기존 시스템의 결제 인터페이스
interface LegacyPaymentProcessor {
void processPayment(String cardNumber, double amount);
}
// 새로운 시스템의 결제 인터페이스
interface ModernPaymentProcessor {
PaymentResult processPayment(PaymentRequest request);
}
// 기존 구현체
class LegacyPaymentProcessorImpl implements LegacyPaymentProcessor {
@Override
public void processPayment(String cardNumber, double amount) {
System.out.println("Legacy: Processing payment of " + amount + " for card " + cardNumber);
}
}
// 새로운 구현체
class ModernPaymentProcessorImpl implements ModernPaymentProcessor {
@Override
public PaymentResult processPayment(PaymentRequest request) {
System.out.println("Modern: Processing payment of " + request.getAmount() +
" for card " + request.getCardNumber());
return new PaymentResult(true, "Payment successful");
}
}
// 새로운 시스템에서 기존 시스템을 사용해야 하는 상황
// → 어댑터 패턴으로 해결!
어댑터 패턴 구현
// ✅ 해결책: 어댑터 패턴
public class PaymentAdapter implements ModernPaymentProcessor {
private final LegacyPaymentProcessor legacyProcessor;
public PaymentAdapter(LegacyPaymentProcessor legacyProcessor) {
this.legacyProcessor = legacyProcessor;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
try {
// 새로운 인터페이스를 기존 인터페이스로 변환
legacyProcessor.processPayment(
request.getCardNumber(),
request.getAmount()
);
return new PaymentResult(true, "Payment processed successfully");
} catch (Exception e) {
return new PaymentResult(false, "Payment failed: " + e.getMessage());
}
}
}
// 지원 클래스들
class PaymentRequest {
private final String cardNumber;
private final double amount;
private final String currency;
public PaymentRequest(String cardNumber, double amount, String currency) {
this.cardNumber = cardNumber;
this.amount = amount;
this.currency = currency;
}
public String getCardNumber() { return cardNumber; }
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
}
class PaymentResult {
private final boolean success;
private final String message;
public PaymentResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
}
// 사용 예제
public class AdapterPatternExample {
public static void main(String[] args) {
// 기존 시스템
LegacyPaymentProcessor legacyProcessor = new LegacyPaymentProcessorImpl();
// 어댑터를 통해 새로운 시스템에서 기존 시스템 사용
ModernPaymentProcessor adapter = new PaymentAdapter(legacyProcessor);
// 새로운 인터페이스로 결제 처리
PaymentRequest request = new PaymentRequest("1234-5678-9012-3456", 100.0, "USD");
PaymentResult result = adapter.processPayment(request);
System.out.println("Payment result: " + result.getMessage());
}
}
실무 활용: 외부 API 어댑터
// 외부 시스템 API (변경 불가)
class ExternalWeatherAPI {
public String getWeatherData(String city) {
// 외부 API 호출 시뮬레이션
return "Temperature: 25°C, Humidity: 60%, City: " + city;
}
}
// 내부 시스템 인터페이스
interface WeatherService {
WeatherInfo getWeatherInfo(String city);
}
// 내부 시스템 데이터 모델
class WeatherInfo {
private final String city;
private final double temperature;
private final double humidity;
private final String description;
public WeatherInfo(String city, double temperature, double humidity, String description) {
this.city = city;
this.temperature = temperature;
this.humidity = humidity;
this.description = description;
}
// Getters
public String getCity() { return city; }
public double getTemperature() { return temperature; }
public double getHumidity() { return humidity; }
public String getDescription() { return description; }
}
// ✅ 어댑터 구현
public class WeatherServiceAdapter implements WeatherService {
private final ExternalWeatherAPI externalAPI;
public WeatherServiceAdapter(ExternalWeatherAPI externalAPI) {
this.externalAPI = externalAPI;
}
@Override
public WeatherInfo getWeatherInfo(String city) {
// 외부 API 호출
String rawData = externalAPI.getWeatherData(city);
// 외부 데이터를 내부 모델로 변환
return parseWeatherData(rawData);
}
private WeatherInfo parseWeatherData(String rawData) {
// 간단한 파싱 로직 (실제로는 더 복잡할 수 있음)
String[] parts = rawData.split(", ");
String city = parts[2].substring(6); // "City: Seoul" -> "Seoul"
double temperature = Double.parseDouble(parts[0].substring(12, 14)); // "Temperature: 25°C" -> 25
double humidity = Double.parseDouble(parts[1].substring(9, 11)); // "Humidity: 60%" -> 60
return new WeatherInfo(city, temperature, humidity, "Sunny");
}
}
// 사용 예제
public class WeatherServiceExample {
public static void main(String[] args) {
ExternalWeatherAPI externalAPI = new ExternalWeatherAPI();
WeatherService weatherService = new WeatherServiceAdapter(externalAPI);
WeatherInfo weather = weatherService.getWeatherInfo("Seoul");
System.out.println("Weather in " + weather.getCity() +
": " + weather.getTemperature() + "°C, " +
weather.getHumidity() + "% humidity");
}
}
데코레이터 패턴: 기능 확장의 유연성
Java I/O의 데코레이터 패턴
// Java I/O에서 데코레이터 패턴 활용
public class JavaIODecoratorExample {
public static void main(String[] args) throws IOException {
// 기본 FileInputStream
FileInputStream fileInputStream = new FileInputStream("data.txt");
// BufferedInputStream으로 감싸기 (버퍼링 기능 추가)
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
// DataInputStream으로 감싸기 (데이터 타입 읽기 기능 추가)
DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);
// 체인: DataInputStream -> BufferedInputStream -> FileInputStream
// 각 데코레이터가 추가 기능을 제공
// 사용
int intValue = dataInputStream.readInt();
String stringValue = dataInputStream.readUTF();
dataInputStream.close();
}
}
실무 활용: 커스텀 데코레이터
// 기본 인터페이스
interface Coffee {
String getDescription();
double getCost();
}
// 기본 구현체
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// 데코레이터 추상 클래스
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// 구체적인 데코레이터들
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return coffee.getCost() + 0.2;
}
}
class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Whip";
}
@Override
public double getCost() {
return coffee.getCost() + 0.7;
}
}
// 사용 예제
public class DecoratorPatternExample {
public static void main(String[] args) {
// 기본 커피
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// 우유 추가
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// 설탕 추가
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// 휘핑크림 추가
coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// 결과:
// Simple Coffee - $2.0
// Simple Coffee, Milk - $2.5
// Simple Coffee, Milk, Sugar - $2.7
// Simple Coffee, Milk, Sugar, Whip - $3.4
}
}
실무 활용: HTTP 요청 데코레이터
// 기본 HTTP 클라이언트 인터페이스
interface HttpClient {
HttpResponse send(HttpRequest request);
}
// 기본 구현체
class BasicHttpClient implements HttpClient {
@Override
public HttpResponse send(HttpRequest request) {
System.out.println("Sending HTTP request to: " + request.getUrl());
return new HttpResponse(200, "OK");
}
}
// 요청/응답 모델
class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
public HttpRequest(String url, String method, Map<String, String> headers, String body) {
this.url = url;
this.method = method;
this.headers = headers != null ? headers : new HashMap<>();
this.body = body;
}
// Getters
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
}
class HttpResponse {
private final int statusCode;
private final String message;
public HttpResponse(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
public int getStatusCode() { return statusCode; }
public String getMessage() { return message; }
}
// 데코레이터 추상 클래스
abstract class HttpClientDecorator implements HttpClient {
protected HttpClient httpClient;
public HttpClientDecorator(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public HttpResponse send(HttpRequest request) {
return httpClient.send(request);
}
}
// 로깅 데코레이터
class LoggingHttpClient extends HttpClientDecorator {
public LoggingHttpClient(HttpClient httpClient) {
super(httpClient);
}
@Override
public HttpResponse send(HttpRequest request) {
System.out.println("Logging: Request to " + request.getUrl());
long startTime = System.currentTimeMillis();
HttpResponse response = httpClient.send(request);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Logging: Response " + response.getStatusCode() +
" in " + duration + "ms");
return response;
}
}
// 인증 데코레이터
class AuthenticationHttpClient extends HttpClientDecorator {
private final String apiKey;
public AuthenticationHttpClient(HttpClient httpClient, String apiKey) {
super(httpClient);
this.apiKey = apiKey;
}
@Override
public HttpResponse send(HttpRequest request) {
// 인증 헤더 추가
Map<String, String> headers = new HashMap<>(request.getHeaders());
headers.put("Authorization", "Bearer " + apiKey);
HttpRequest authenticatedRequest = new HttpRequest(
request.getUrl(),
request.getMethod(),
headers,
request.getBody()
);
return httpClient.send(authenticatedRequest);
}
}
// 재시도 데코레이터
class RetryHttpClient extends HttpClientDecorator {
private final int maxRetries;
public RetryHttpClient(HttpClient httpClient, int maxRetries) {
super(httpClient);
this.maxRetries = maxRetries;
}
@Override
public HttpResponse send(HttpRequest request) {
int attempts = 0;
Exception lastException = null;
while (attempts <= maxRetries) {
try {
HttpResponse response = httpClient.send(request);
if (response.getStatusCode() < 500) {
return response; // 성공 또는 클라이언트 에러
}
} catch (Exception e) {
lastException = e;
}
attempts++;
if (attempts <= maxRetries) {
System.out.println("Retry attempt " + attempts + " for " + request.getUrl());
try {
Thread.sleep(1000 * attempts); // 지수 백오프
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
throw new RuntimeException("Max retries exceeded", lastException);
}
}
// 사용 예제
public class HttpClientDecoratorExample {
public static void main(String[] args) {
// 기본 클라이언트
HttpClient client = new BasicHttpClient();
// 데코레이터 체인 구성
client = new LoggingHttpClient(client);
client = new AuthenticationHttpClient(client, "your-api-key");
client = new RetryHttpClient(client, 3);
// 요청 생성
HttpRequest request = new HttpRequest(
"https://api.example.com/data",
"GET",
new HashMap<>(),
null
);
// 체인된 데코레이터들로 요청 처리
HttpResponse response = client.send(request);
System.out.println("Final response: " + response.getStatusCode());
}
}
7.3 행위 패턴
전략 패턴과 템플릿 메서드 패턴
전략 패턴: 알고리즘 캡슐화
실무에서 마주한 전략 패턴 케이스
// ❌ 문제: 조건문으로 분기 처리
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if ("CREDIT_CARD".equals(paymentType)) {
System.out.println("Processing credit card payment: " + amount);
// 신용카드 처리 로직
} else if ("PAYPAL".equals(paymentType)) {
System.out.println("Processing PayPal payment: " + amount);
// PayPal 처리 로직
} else if ("BANK_TRANSFER".equals(paymentType)) {
System.out.println("Processing bank transfer: " + amount);
// 은행 이체 처리 로직
} else {
throw new IllegalArgumentException("Unsupported payment type: " + paymentType);
}
}
}
// 문제점:
// 1. 새로운 결제 방식 추가 시 코드 수정 필요
// 2. 각 결제 방식의 로직이 섞여있음
// 3. 테스트하기 어려움
// 4. 단일 책임 원칙 위반
전략 패턴 구현
// ✅ 해결책: 전략 패턴
// 전략 인터페이스
interface PaymentStrategy {
PaymentResult processPayment(double amount);
String getPaymentType();
}
// 구체적인 전략들
class CreditCardPaymentStrategy implements PaymentStrategy {
private final String cardNumber;
private final String cvv;
public CreditCardPaymentStrategy(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public PaymentResult processPayment(double amount) {
System.out.println("Processing credit card payment: " + amount);
System.out.println("Card: " + maskCardNumber(cardNumber));
// 실제 신용카드 처리 로직
boolean success = validateCard() && processCardPayment(amount);
return new PaymentResult(success, success ? "Credit card payment successful" : "Credit card payment failed");
}
@Override
public String getPaymentType() {
return "CREDIT_CARD";
}
private boolean validateCard() {
// 카드 유효성 검증
return cardNumber.length() == 16 && cvv.length() == 3;
}
private boolean processCardPayment(double amount) {
// 실제 카드 결제 처리
return true; // 시뮬레이션
}
private String maskCardNumber(String cardNumber) {
return cardNumber.substring(0, 4) + "****" + cardNumber.substring(12);
}
}
class PayPalPaymentStrategy implements PaymentStrategy {
private final String email;
private final String password;
public PayPalPaymentStrategy(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public PaymentResult processPayment(double amount) {
System.out.println("Processing PayPal payment: " + amount);
System.out.println("Email: " + email);
// 실제 PayPal 처리 로직
boolean success = authenticatePayPal() && processPayPalPayment(amount);
return new PaymentResult(success, success ? "PayPal payment successful" : "PayPal payment failed");
}
@Override
public String getPaymentType() {
return "PAYPAL";
}
private boolean authenticatePayPal() {
// PayPal 인증
return email.contains("@") && password.length() >= 6;
}
private boolean processPayPalPayment(double amount) {
// 실제 PayPal 결제 처리
return true; // 시뮬레이션
}
}
class BankTransferPaymentStrategy implements PaymentStrategy {
private final String accountNumber;
private final String routingNumber;
public BankTransferPaymentStrategy(String accountNumber, String routingNumber) {
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
}
@Override
public PaymentResult processPayment(double amount) {
System.out.println("Processing bank transfer: " + amount);
System.out.println("Account: " + maskAccountNumber(accountNumber));
// 실제 은행 이체 처리 로직
boolean success = validateAccount() && processBankTransfer(amount);
return new PaymentResult(success, success ? "Bank transfer successful" : "Bank transfer failed");
}
@Override
public String getPaymentType() {
return "BANK_TRANSFER";
}
private boolean validateAccount() {
// 계좌 유효성 검증
return accountNumber.length() == 10 && routingNumber.length() == 9;
}
private boolean processBankTransfer(double amount) {
// 실제 은행 이체 처리
return true; // 시뮬레이션
}
private String maskAccountNumber(String accountNumber) {
return "****" + accountNumber.substring(4);
}
}
// 컨텍스트 클래스
class PaymentProcessor {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public PaymentResult processPayment(double amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("Payment strategy not set");
}
System.out.println("Using payment strategy: " + paymentStrategy.getPaymentType());
return paymentStrategy.processPayment(amount);
}
}
// 결과 클래스
class PaymentResult {
private final boolean success;
private final String message;
public PaymentResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
}
// 사용 예제
public class StrategyPatternExample {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
// 신용카드 결제
processor.setPaymentStrategy(new CreditCardPaymentStrategy("1234567890123456", "123"));
PaymentResult result1 = processor.processPayment(100.0);
System.out.println("Result: " + result1.getMessage());
// PayPal 결제
processor.setPaymentStrategy(new PayPalPaymentStrategy("user@example.com", "password123"));
PaymentResult result2 = processor.processPayment(50.0);
System.out.println("Result: " + result2.getMessage());
// 은행 이체
processor.setPaymentStrategy(new BankTransferPaymentStrategy("1234567890", "123456789"));
PaymentResult result3 = processor.processPayment(200.0);
System.out.println("Result: " + result3.getMessage());
}
}
실무 활용: 정렬 알고리즘 전략
// 정렬 전략 인터페이스
interface SortingStrategy<T extends Comparable<T>> {
void sort(List<T> list);
String getAlgorithmName();
}
// 버블 정렬 전략
class BubbleSortStrategy<T extends Comparable<T>> implements SortingStrategy<T> {
@Override
public void sort(List<T> list) {
int n = list.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (list.get(j).compareTo(list.get(j + 1)) > 0) {
T temp = list.get(j);
list.set(j, list.get(j + 1));
list.set(j + 1, temp);
}
}
}
}
@Override
public String getAlgorithmName() {
return "Bubble Sort";
}
}
// 퀵 정렬 전략
class QuickSortStrategy<T extends Comparable<T>> implements SortingStrategy<T> {
@Override
public void sort(List<T> list) {
quickSort(list, 0, list.size() - 1);
}
private void quickSort(List<T> list, int low, int high) {
if (low < high) {
int pivotIndex = partition(list, low, high);
quickSort(list, low, pivotIndex - 1);
quickSort(list, pivotIndex + 1, high);
}
}
private int partition(List<T> list, int low, int high) {
T pivot = list.get(high);
int i = low - 1;
for (int j = low; j < high; j++) {
if (list.get(j).compareTo(pivot) <= 0) {
i++;
swap(list, i, j);
}
}
swap(list, i + 1, high);
return i + 1;
}
private void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
@Override
public String getAlgorithmName() {
return "Quick Sort";
}
}
// 병합 정렬 전략
class MergeSortStrategy<T extends Comparable<T>> implements SortingStrategy<T> {
@Override
public void sort(List<T> list) {
mergeSort(list, 0, list.size() - 1);
}
private void mergeSort(List<T> list, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(list, left, mid);
mergeSort(list, mid + 1, right);
merge(list, left, mid, right);
}
}
private void merge(List<T> list, int left, int mid, int right) {
List<T> leftArray = new ArrayList<>();
List<T> rightArray = new ArrayList<>();
for (int i = left; i <= mid; i++) {
leftArray.add(list.get(i));
}
for (int i = mid + 1; i <= right; i++) {
rightArray.add(list.get(i));
}
int i = 0, j = 0, k = left;
while (i < leftArray.size() && j < rightArray.size()) {
if (leftArray.get(i).compareTo(rightArray.get(j)) <= 0) {
list.set(k, leftArray.get(i));
i++;
} else {
list.set(k, rightArray.get(j));
j++;
}
k++;
}
while (i < leftArray.size()) {
list.set(k, leftArray.get(i));
i++;
k++;
}
while (j < rightArray.size()) {
list.set(k, rightArray.get(j));
j++;
k++;
}
}
@Override
public String getAlgorithmName() {
return "Merge Sort";
}
}
// 정렬 컨텍스트
class Sorter<T extends Comparable<T>> {
private SortingStrategy<T> strategy;
public void setStrategy(SortingStrategy<T> strategy) {
this.strategy = strategy;
}
public void sort(List<T> list) {
if (strategy == null) {
throw new IllegalStateException("Sorting strategy not set");
}
long startTime = System.nanoTime();
strategy.sort(list);
long endTime = System.nanoTime();
System.out.println(strategy.getAlgorithmName() + " completed in " +
(endTime - startTime) / 1_000_000 + "ms");
}
}
// 사용 예제
public class SortingStrategyExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(64, 34, 25, 12, 22, 11, 90);
Sorter<Integer> sorter = new Sorter<>();
// 버블 정렬
List<Integer> bubbleList = new ArrayList<>(numbers);
sorter.setStrategy(new BubbleSortStrategy<>());
sorter.sort(bubbleList);
System.out.println("Bubble sort result: " + bubbleList);
// 퀵 정렬
List<Integer> quickList = new ArrayList<>(numbers);
sorter.setStrategy(new QuickSortStrategy<>());
sorter.sort(quickList);
System.out.println("Quick sort result: " + quickList);
// 병합 정렬
List<Integer> mergeList = new ArrayList<>(numbers);
sorter.setStrategy(new MergeSortStrategy<>());
sorter.sort(mergeList);
System.out.println("Merge sort result: " + mergeList);
}
}
템플릿 메서드 패턴: 공통 로직 추출
실무에서 마주한 템플릿 메서드 패턴 케이스
// ❌ 문제: 중복된 코드
class DatabaseUserService {
public User createUser(UserRequest request) {
// 1. 요청 검증
validateRequest(request);
// 2. 데이터베이스 연결
Connection conn = getConnection();
// 3. 트랜잭션 시작
conn.setAutoCommit(false);
try {
// 4. 사용자 생성
User user = insertUser(conn, request);
// 5. 로그 기록
logUserCreation(user);
// 6. 트랜잭션 커밋
conn.commit();
return user;
} catch (Exception e) {
// 7. 트랜잭션 롤백
conn.rollback();
throw e;
} finally {
// 8. 연결 해제
conn.close();
}
}
public User updateUser(Long userId, UserRequest request) {
// 1. 요청 검증
validateRequest(request);
// 2. 데이터베이스 연결
Connection conn = getConnection();
// 3. 트랜잭션 시작
conn.setAutoCommit(false);
try {
// 4. 사용자 업데이트
User user = updateUserInDb(conn, userId, request);
// 5. 로그 기록
logUserUpdate(user);
// 6. 트랜잭션 커밋
conn.commit();
return user;
} catch (Exception e) {
// 7. 트랜잭션 롤백
conn.rollback();
throw e;
} finally {
// 8. 연결 해제
conn.close();
}
}
// 중복된 코드가 많음!
}
템플릿 메서드 패턴 구현
// ✅ 해결책: 템플릿 메서드 패턴
abstract class DatabaseServiceTemplate<T, R> {
// 템플릿 메서드 (공통 로직)
public final T execute(R request) {
// 1. 요청 검증
validateRequest(request);
// 2. 데이터베이스 연결
Connection conn = getConnection();
// 3. 트랜잭션 시작
conn.setAutoCommit(false);
try {
// 4. 비즈니스 로직 실행 (하위 클래스에서 구현)
T result = executeBusinessLogic(conn, request);
// 5. 후처리 (하위 클래스에서 구현)
postProcess(result);
// 6. 트랜잭션 커밋
conn.commit();
return result;
} catch (Exception e) {
// 7. 트랜잭션 롤백
conn.rollback();
throw new RuntimeException("Database operation failed", e);
} finally {
// 8. 연결 해제
closeConnection(conn);
}
}
// 공통 메서드들
protected void validateRequest(R request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
}
protected Connection getConnection() {
try {
return DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
} catch (SQLException e) {
throw new RuntimeException("Failed to get database connection", e);
}
}
protected void closeConnection(Connection conn) {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
System.err.println("Failed to close connection: " + e.getMessage());
}
}
// 추상 메서드들 (하위 클래스에서 구현)
protected abstract T executeBusinessLogic(Connection conn, R request);
protected abstract void postProcess(T result);
}
// 구체적인 구현체들
class UserService extends DatabaseServiceTemplate<User, UserRequest> {
@Override
protected User executeBusinessLogic(Connection conn, UserRequest request) {
// 사용자 생성 로직
System.out.println("Creating user: " + request.getName());
// 실제 DB 삽입 로직 (시뮬레이션)
return new User(1L, request.getName(), request.getEmail());
}
@Override
protected void postProcess(User user) {
// 사용자 생성 후처리
System.out.println("Logging user creation: " + user.getName());
sendWelcomeEmail(user);
}
private void sendWelcomeEmail(User user) {
System.out.println("Sending welcome email to: " + user.getEmail());
}
}
class OrderService extends DatabaseServiceTemplate<Order, OrderRequest> {
@Override
protected Order executeBusinessLogic(Connection conn, OrderRequest request) {
// 주문 생성 로직
System.out.println("Creating order for: " + request.getCustomerId());
// 실제 DB 삽입 로직 (시뮬레이션)
return new Order(1L, request.getCustomerId(), request.getAmount());
}
@Override
protected void postProcess(Order order) {
// 주문 생성 후처리
System.out.println("Logging order creation: " + order.getId());
updateInventory(order);
sendOrderConfirmation(order);
}
private void updateInventory(Order order) {
System.out.println("Updating inventory for order: " + order.getId());
}
private void sendOrderConfirmation(Order order) {
System.out.println("Sending order confirmation for: " + order.getId());
}
}
// 지원 클래스들
class UserRequest {
private final String name;
private final String email;
public UserRequest(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}
class User {
private final Long id;
private final String name;
private final String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
class OrderRequest {
private final Long customerId;
private final double amount;
public OrderRequest(Long customerId, double amount) {
this.customerId = customerId;
this.amount = amount;
}
public Long getCustomerId() { return customerId; }
public double getAmount() { return amount; }
}
class Order {
private final Long id;
private final Long customerId;
private final double amount;
public Order(Long id, Long customerId, double amount) {
this.id = id;
this.customerId = customerId;
this.amount = amount;
}
public Long getId() { return id; }
public Long getCustomerId() { return customerId; }
public double getAmount() { return amount; }
}
// 사용 예제
public class TemplateMethodPatternExample {
public static void main(String[] args) {
// 사용자 서비스
UserService userService = new UserService();
UserRequest userRequest = new UserRequest("홍길동", "hong@example.com");
User user = userService.execute(userRequest);
System.out.println("Created user: " + user.getName());
// 주문 서비스
OrderService orderService = new OrderService();
OrderRequest orderRequest = new OrderRequest(1L, 100.0);
Order order = orderService.execute(orderRequest);
System.out.println("Created order: " + order.getId());
}
}
옵저버 패턴으로 이벤트 처리
발행-구독 모델 구현
Java의 Observable (Deprecated) 문제점
// ❌ 문제: Java의 Observable 클래스 (Deprecated)
import java.util.Observable;
import java.util.Observer;
class NewsAgency extends Observable {
private String news;
public void setNews(String news) {
this.news = news;
setChanged(); // 상태 변경 알림
notifyObservers(news); // 관찰자들에게 알림
}
}
class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println(name + " received news: " + arg);
}
}
// 문제점:
// 1. Observable은 클래스이므로 상속 필요
// 2. setChanged() 메서드가 protected
// 3. 타입 안전성 부족
// 4. Java 9부터 deprecated
현대적인 옵저버 패턴 구현
// ✅ 해결책: 현대적인 옵저버 패턴
// 이벤트 인터페이스
interface Event {
String getEventType();
long getTimestamp();
}
// 구체적인 이벤트들
class UserRegisteredEvent implements Event {
private final String userId;
private final String email;
private final long timestamp;
public UserRegisteredEvent(String userId, String email) {
this.userId = userId;
this.email = email;
this.timestamp = System.currentTimeMillis();
}
@Override
public String getEventType() {
return "USER_REGISTERED";
}
@Override
public long getTimestamp() {
return timestamp;
}
public String getUserId() { return userId; }
public String getEmail() { return email; }
}
class OrderCreatedEvent implements Event {
private final String orderId;
private final String customerId;
private final double amount;
private final long timestamp;
public OrderCreatedEvent(String orderId, String customerId, double amount) {
this.orderId = orderId;
this.customerId = customerId;
this.amount = amount;
this.timestamp = System.currentTimeMillis();
}
@Override
public String getEventType() {
return "ORDER_CREATED";
}
@Override
public long getTimestamp() {
return timestamp;
}
public String getOrderId() { return orderId; }
public String getCustomerId() { return customerId; }
public double getAmount() { return amount; }
}
// 옵저버 인터페이스
interface Observer<T extends Event> {
void handle(T event);
String getObserverName();
}
// 이벤트 발행자
class EventPublisher {
private final Map<String, List<Observer<?>>> observers = new ConcurrentHashMap<>();
public <T extends Event> void subscribe(String eventType, Observer<T> observer) {
observers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(observer);
}
public <T extends Event> void unsubscribe(String eventType, Observer<T> observer) {
List<Observer<?>> eventObservers = observers.get(eventType);
if (eventObservers != null) {
eventObservers.remove(observer);
}
}
public <T extends Event> void publish(T event) {
List<Observer<?>> eventObservers = observers.get(event.getEventType());
if (eventObservers != null) {
for (Observer<?> observer : eventObservers) {
try {
@SuppressWarnings("unchecked")
Observer<T> typedObserver = (Observer<T>) observer;
typedObserver.handle(event);
} catch (Exception e) {
System.err.println("Error handling event in observer " +
observer.getObserverName() + ": " + e.getMessage());
}
}
}
}
public void publishAsync(Event event) {
CompletableFuture.runAsync(() -> publish(event));
}
}
// 구체적인 옵저버들
class EmailNotificationObserver implements Observer<UserRegisteredEvent> {
@Override
public void handle(UserRegisteredEvent event) {
System.out.println("Sending welcome email to: " + event.getEmail());
// 실제 이메일 전송 로직
}
@Override
public String getObserverName() {
return "EmailNotificationObserver";
}
}
class UserAnalyticsObserver implements Observer<UserRegisteredEvent> {
@Override
public void handle(UserRegisteredEvent event) {
System.out.println("Recording user registration analytics: " + event.getUserId());
// 실제 분석 데이터 기록 로직
}
@Override
public String getObserverName() {
return "UserAnalyticsObserver";
}
}
class InventoryObserver implements Observer<OrderCreatedEvent> {
@Override
public void handle(OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.getOrderId());
// 실제 재고 업데이트 로직
}
@Override
public String getObserverName() {
return "InventoryObserver";
}
}
class OrderNotificationObserver implements Observer<OrderCreatedEvent> {
@Override
public void handle(OrderCreatedEvent event) {
System.out.println("Sending order confirmation to customer: " + event.getCustomerId());
// 실제 주문 확인 이메일 전송 로직
}
@Override
public String getObserverName() {
return "OrderNotificationObserver";
}
}
// 사용 예제
public class ObserverPatternExample {
public static void main(String[] args) {
EventPublisher publisher = new EventPublisher();
// 옵저버 등록
EmailNotificationObserver emailObserver = new EmailNotificationObserver();
UserAnalyticsObserver analyticsObserver = new UserAnalyticsObserver();
publisher.subscribe("USER_REGISTERED", emailObserver);
publisher.subscribe("USER_REGISTERED", analyticsObserver);
InventoryObserver inventoryObserver = new InventoryObserver();
OrderNotificationObserver orderObserver = new OrderNotificationObserver();
publisher.subscribe("ORDER_CREATED", inventoryObserver);
publisher.subscribe("ORDER_CREATED", orderObserver);
// 이벤트 발행
UserRegisteredEvent userEvent = new UserRegisteredEvent("user123", "user@example.com");
publisher.publish(userEvent);
OrderCreatedEvent orderEvent = new OrderCreatedEvent("order456", "customer789", 150.0);
publisher.publish(orderEvent);
// 비동기 이벤트 발행
publisher.publishAsync(new UserRegisteredEvent("user456", "async@example.com"));
// 잠시 대기 (비동기 처리 확인)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
실무 활용: Spring Event 시스템
// Spring의 이벤트 시스템 활용
@Component
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public User createUser(String name, String email) {
// 사용자 생성 로직
User user = new User(name, email);
// 이벤트 발행
UserRegisteredEvent event = new UserRegisteredEvent(user.getId(), user.getEmail());
eventPublisher.publishEvent(event);
return user;
}
}
// Spring 이벤트 리스너
@Component
public class UserEventListener {
@EventListener
@Async
public void handleUserRegistered(UserRegisteredEvent event) {
System.out.println("Async handling user registration: " + event.getUserId());
// 비동기 처리 로직
}
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("Handling order creation: " + event.getOrderId());
// 동기 처리 로직
}
}
마치며
이번 글에서는 실무에서 자주 사용되는 디자인 패턴들을 Java 관점에서 깊이 있게 다뤄보았습니다.
핵심 요약:
- 싱글톤 패턴: Thread-Safe한 구현 방법과 Spring 싱글톤과의 차이점
- 빌더 패턴: 가독성과 유지보수성을 높이는 객체 생성 방법
- 어댑터 패턴: 레거시 코드 통합과 인터페이스 호환성
- 데코레이터 패턴: 기능 확장의 유연성과 Java I/O 활용
- 전략 패턴: 알고리즘 캡슐화와 런타임 전략 변경
- 템플릿 메서드 패턴: 공통 로직 추출과 코드 중복 제거
- 옵저버 패턴: 이벤트 처리와 발행-구독 모델
실무에서는 단순히 패턴을 아는 것을 넘어, 언제 사용해야 하는지, 어떤 트레이드오프가 있는지를 이해하는 것이 중요합니다.
다음 단계에서는:
- 최신 Java 기능 (Java 17+)
- 실전 프로젝트와 베스트 프랙티스
를 다룰 예정입니다.
'Java' 카테고리의 다른 글
| 9단계: 실전 프로젝트 & 베스트 프랙티스 (0) | 2025.12.11 |
|---|---|
| 8단계: Java 기능 (Java 17+) (0) | 2025.12.11 |
| 6단계: JVM & 성능 최적화 (0) | 2025.12.11 |
| 5단계: Java 동시성 & 멀티스레딩 (0) | 2025.12.11 |
| 4단계: Java 함수형 프로그래밍 (Java 8+) (0) | 2025.12.11 |