Repository

7단계: 디자인 패턴 & 아키텍처 본문

Java

7단계: 디자인 패턴 & 아키텍처

Mr.Manager 2025. 12. 11. 20:24
반응형

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 관점에서 깊이 있게 다뤄보았습니다.

핵심 요약:

  1. 싱글톤 패턴: Thread-Safe한 구현 방법과 Spring 싱글톤과의 차이점
  2. 빌더 패턴: 가독성과 유지보수성을 높이는 객체 생성 방법
  3. 어댑터 패턴: 레거시 코드 통합과 인터페이스 호환성
  4. 데코레이터 패턴: 기능 확장의 유연성과 Java I/O 활용
  5. 전략 패턴: 알고리즘 캡슐화와 런타임 전략 변경
  6. 템플릿 메서드 패턴: 공통 로직 추출과 코드 중복 제거
  7. 옵저버 패턴: 이벤트 처리와 발행-구독 모델

실무에서는 단순히 패턴을 아는 것을 넘어, 언제 사용해야 하는지, 어떤 트레이드오프가 있는지를 이해하는 것이 중요합니다.

다음 단계에서는:

  • 최신 Java 기능 (Java 17+)
  • 실전 프로젝트와 베스트 프랙티스

를 다룰 예정입니다.

반응형