Repository

2단계: Java 객체지향 프로그래밍 본문

Java

2단계: Java 객체지향 프로그래밍

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

2단계: Java 객체지향 프로그래밍

4년간의 실무에서 마주한 객체지향의 진짜 모습을 담았습니다. 단순한 문법이 아닌, '좋은 설계'가 무엇인지, '왜' 그렇게 해야 하는지를 깊이 있게 다룹니다.


2.1 클래스와 객체

클래스 설계의 기본 원칙

생성자 오버로딩: 유연성과 명확성의 균형

기본 생성자의 함정

public class User {
    private String name;
    private String email;
    private int age;
    private String address;
    private String phone;

    // ❌ 나쁜 예: 너무 많은 파라미터
    public User(String name, String email, int age, String address, String phone) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.address = address;
        this.phone = phone;
    }

    // 호출 시 가독성 문제
    User user = new User("홍길동", "hong@email.com", 30, "서울시", "010-1234-5678");
    // 순서를 헷갈릴 위험!
}

해결책 1: 생성자 오버로딩

public class User {
    private String name;
    private String email;
    private int age;
    private String address;
    private String phone;

    // 필수 필드만 받는 생성자
    public User(String name, String email) {
        this(name, email, 0, null, null);
    }

    // 나이까지 받는 생성자
    public User(String name, String email, int age) {
        this(name, email, age, null, null);
    }

    // 모든 필드를 받는 생성자
    public User(String name, String email, int age, String address, String phone) {
        validateEmail(email);
        this.name = name;
        this.email = email;
        this.age = age;
        this.address = address;
        this.phone = phone;
    }

    private void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email: " + email);
        }
    }
}

// 사용
User user1 = new User("홍길동", "hong@email.com");
User user2 = new User("김철수", "kim@email.com", 30);

해결책 2: Builder 패턴 (권장)

public class User {
    private final String name;        // final로 불변성 보장
    private final String email;
    private final int age;
    private final String address;
    private final String phone;

    // 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;
    }

    public static class Builder {
        // 필수 파라미터
        private final String name;
        private final String email;

        // 선택 파라미터 (기본값)
        private int age = 0;
        private String address = "";
        private String phone = "";

        public Builder(String name, String email) {
            if (name == null || email == null) {
                throw new IllegalArgumentException("Name and email are required");
            }
            this.name = name;
            this.email = email;
        }

        public Builder age(int age) {
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("Invalid age: " + age);
            }
            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 User build() {
            // 빌드 전 최종 검증
            validateEmail(email);
            return new User(this);
        }

        private void validateEmail(String email) {
            if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
                throw new IllegalArgumentException("Invalid email format: " + email);
            }
        }
    }

    // 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; }
}

// 사용 - 가독성이 매우 좋음!
User user = new User.Builder("홍길동", "hong@email.com")
    .age(30)
    .address("서울시 강남구")
    .phone("010-1234-5678")
    .build();

Lombok을 사용한 Builder

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class User {
    private final String name;
    private final String email;

    @Builder.Default
    private final int age = 0;

    @Builder.Default
    private final String address = "";

    @Builder.Default
    private final String phone = "";
}

// 사용
User user = User.builder()
    .name("홍길동")
    .email("hong@email.com")
    .age(30)
    .build();

this vs super: 생성자 체이닝의 비밀

this() - 같은 클래스의 다른 생성자 호출

public class Rectangle {
    private int width;
    private int height;
    private String color;

    // 정사각형 생성 (한 변의 길이만)
    public Rectangle(int size) {
        this(size, size, "white");  // 다른 생성자 호출
    }

    // 크기만 지정
    public Rectangle(int width, int height) {
        this(width, height, "white");
    }

    // 모든 속성 지정 (메인 생성자)
    public Rectangle(int width, int height, String color) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Width and height must be positive");
        }
        this.width = width;
        this.height = height;
        this.color = color;

        // 초기화 로직은 한 곳에만!
        System.out.println("Rectangle created: " + width + "x" + height);
    }

    public int getArea() {
        return width * height;
    }
}

// 사용
Rectangle square = new Rectangle(10);              // 10x10 정사각형
Rectangle rect = new Rectangle(10, 20);            // 10x20 직사각형
Rectangle coloredRect = new Rectangle(10, 20, "red"); // 빨간 직사각형

super() - 부모 클래스의 생성자 호출

public class Shape {
    private String color;
    private boolean filled;

    public Shape() {
        this("white", false);
    }

    public Shape(String color, boolean filled) {
        this.color = color;
        this.filled = filled;
        System.out.println("Shape created");
    }

    public String getColor() { return color; }
    public boolean isFilled() { return filled; }
}

public class Circle extends Shape {
    private double radius;

    // 기본 생성자
    public Circle(double radius) {
        super();  // Shape() 호출 - 생략 가능 (컴파일러가 자동 추가)
        this.radius = radius;
    }

    // 모든 속성 지정
    public Circle(double radius, String color, boolean filled) {
        super(color, filled);  // Shape(String, boolean) 호출
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius must be positive");
        }
        this.radius = radius;
        System.out.println("Circle created");
    }

    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// 실행 순서 확인
Circle circle = new Circle(5.0, "red", true);
// 출력:
// Shape created
// Circle created

중요: super()와 this()의 규칙

public class MyClass extends ParentClass {
    private int value;

    public MyClass() {
        // ❌ 오류: super() 또는 this()는 첫 줄에 와야 함
        int temp = 10;
        super();
    }

    public MyClass(int value) {
        // ❌ 오류: super()와 this()를 동시에 사용 불가
        super();
        this();
    }

    // ✅ 올바른 방법
    public MyClass(int value) {
        super();  // 첫 줄에 위치
        this.value = value;
    }

    // ✅ 초기화가 필요하면 별도 메서드 사용
    public MyClass(int value, boolean initialize) {
        super();
        this.value = value;
        if (initialize) {
            init();  // 초기화 로직을 메서드로 분리
        }
    }

    private void init() {
        // 복잡한 초기화 로직
    }
}

실무 패턴: Factory Method와 생성자

public class DatabaseConnection {
    private String url;
    private String username;
    private String password;
    private int poolSize;

    // private 생성자 - 외부에서 직접 생성 불가
    private DatabaseConnection(String url, String username, String password, int poolSize) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.poolSize = poolSize;
    }

    // Factory Methods
    public static DatabaseConnection forDevelopment() {
        return new DatabaseConnection(
            "jdbc:h2:mem:testdb",
            "sa",
            "",
            5
        );
    }

    public static DatabaseConnection forProduction(String url, String username, String password) {
        return new DatabaseConnection(
            url,
            username,
            password,
            20  // 프로덕션은 큰 풀 사이즈
        );
    }

    public static DatabaseConnection forTest() {
        return new DatabaseConnection(
            "jdbc:h2:mem:test",
            "test",
            "test",
            1  // 테스트는 단일 연결
        );
    }
}

// 사용 - 의도가 명확함
DatabaseConnection devConn = DatabaseConnection.forDevelopment();
DatabaseConnection prodConn = DatabaseConnection.forProduction(
    "jdbc:mysql://prod-server:3306/mydb",
    "admin",
    "secret"
);

접근 제어자의 올바른 사용

4가지 접근 제어자 비교

제어자 같은 클래스 같은 패키지 자식 클래스 모든 곳
private
default
protected
public

실무 가이드라인

package com.example.user;

public class User {
    // ✅ private: 외부 노출 불필요한 필드
    private Long id;
    private String passwordHash;  // 민감 정보는 반드시 private
    private LocalDateTime lastLoginAt;

    // ✅ protected: 자식 클래스에서 접근 필요
    protected String email;
    protected UserStatus status;

    // ❌ public 필드는 거의 사용하지 않음
    // public String name;  // Getter/Setter 사용!

    // ✅ public: 외부에서 사용할 메서드
    public void login(String password) {
        if (verifyPassword(password)) {
            this.lastLoginAt = LocalDateTime.now();
        }
    }

    // ✅ private: 내부 구현 로직
    private boolean verifyPassword(String password) {
        // BCrypt 등을 사용한 검증
        return BCrypt.checkpw(password, this.passwordHash);
    }

    // ✅ protected: 자식 클래스에서 재정의 가능
    protected void onLogin() {
        // 로그인 후처리 로직
        // AdminUser에서 오버라이드 가능
    }

    // ✅ default (package-private): 같은 패키지 내 테스트용
    void setPasswordHashForTest(String hash) {
        this.passwordHash = hash;
    }

    // ✅ public getter: 읽기 허용
    public String getEmail() {
        return email;
    }

    // ✅ package-private setter: 같은 패키지에서만 수정
    void setEmail(String email) {
        this.email = email;
    }
}

캡슐화 베스트 프랙티스

// ❌ 나쁜 예: 내부 컬렉션을 그대로 노출
public class Team {
    private List<User> members = new ArrayList<>();

    public List<User> getMembers() {
        return members;  // 외부에서 members.clear() 가능!
    }
}

// ✅ 좋은 예: 방어적 복사
public class Team {
    private List<User> members = new ArrayList<>();

    // 읽기 전용 뷰 반환
    public List<User> getMembers() {
        return Collections.unmodifiableList(members);
    }

    // 또는 복사본 반환
    public List<User> getMembersCopy() {
        return new ArrayList<>(members);
    }

    // 멤버 추가는 메서드로 제어
    public void addMember(User user) {
        if (user == null) {
            throw new IllegalArgumentException("User cannot be null");
        }
        if (members.size() >= 100) {
            throw new IllegalStateException("Team is full");
        }
        members.add(user);
    }
}

가변 객체 필드의 방어적 복사

public class Event {
    private Date eventDate;  // Date는 mutable!

    // ❌ 나쁜 예
    public Event(Date eventDate) {
        this.eventDate = eventDate;  // 외부 참조 그대로 저장
    }

    public Date getEventDate() {
        return eventDate;  // 외부에서 수정 가능!
    }
}

// 문제 발생!
Date date = new Date();
Event event = new Event(date);
date.setTime(0);  // event의 eventDate도 변경됨!

// ✅ 좋은 예
public class Event {
    private final Date eventDate;

    public Event(Date eventDate) {
        // 방어적 복사
        this.eventDate = new Date(eventDate.getTime());
    }

    public Date getEventDate() {
        // 복사본 반환
        return new Date(eventDate.getTime());
    }
}

// ✅✅ 최선: 불변 객체 사용 (Java 8+)
import java.time.LocalDateTime;

public class Event {
    private final LocalDateTime eventDateTime;  // Immutable!

    public Event(LocalDateTime eventDateTime) {
        this.eventDateTime = eventDateTime;  // 복사 불필요
    }

    public LocalDateTime getEventDateTime() {
        return eventDateTime;  // 불변이므로 안전
    }
}

정적(static) 멤버 완벽 이해하기

static 변수 vs 인스턴스 변수: 메모리 관점

메모리 배치

public class Counter {
    // static 변수: Method Area(Metaspace)에 저장
    private static int totalCount = 0;

    // 인스턴스 변수: Heap의 각 객체에 저장
    private int instanceCount = 0;

    public Counter() {
        totalCount++;      // 모든 인스턴스가 공유
        instanceCount++;   // 각 인스턴스마다 별도
    }

    public void printCounts() {
        System.out.println("Total: " + totalCount + ", Instance: " + instanceCount);
    }

    public static int getTotalCount() {
        return totalCount;
    }
}

// 실행
Counter c1 = new Counter();
c1.printCounts();  // Total: 1, Instance: 1

Counter c2 = new Counter();
c2.printCounts();  // Total: 2, Instance: 1

Counter c3 = new Counter();
c3.printCounts();  // Total: 3, Instance: 1

System.out.println(Counter.getTotalCount());  // 3

메모리 다이어그램

[Method Area / Metaspace]
┌─────────────────────────┐
│ Counter.class           │
│  - totalCount = 3       │ ← 모든 인스턴스가 공유
└─────────────────────────┘

[Heap]
┌─────────────────┐
│ Counter@1234    │
│  - instanceCount = 1 │
└─────────────────┘
┌─────────────────┐
│ Counter@5678    │
│  - instanceCount = 1 │
└─────────────────┘
┌─────────────────┐
│ Counter@9abc    │
│  - instanceCount = 1 │
└─────────────────┘

실무 활용: 설정 관리

public class DatabaseConfig {
    // static으로 설정 값 공유
    private static String dbUrl;
    private static String dbUser;
    private static String dbPassword;
    private static int maxConnections = 10;

    // static 초기화 블록
    static {
        // 환경 변수나 설정 파일에서 읽기
        dbUrl = System.getenv("DB_URL");
        dbUser = System.getenv("DB_USER");
        dbPassword = System.getenv("DB_PASSWORD");

        String maxConn = System.getenv("MAX_CONNECTIONS");
        if (maxConn != null) {
            maxConnections = Integer.parseInt(maxConn);
        }

        System.out.println("Database configuration loaded");
    }

    // private 생성자 - 인스턴스화 방지
    private DatabaseConfig() {}

    public static String getDbUrl() {
        return dbUrl;
    }

    public static int getMaxConnections() {
        return maxConnections;
    }
}

// 사용 - 인스턴스 생성 없이
String url = DatabaseConfig.getDbUrl();

static 메서드의 제약사항

기본 규칙

public class MyClass {
    private static int staticVar = 10;
    private int instanceVar = 20;

    // static 메서드
    public static void staticMethod() {
        // ✅ static 변수 접근 가능
        System.out.println(staticVar);

        // ❌ 인스턴스 변수 접근 불가
        // System.out.println(instanceVar);  // 컴파일 에러!

        // ❌ this, super 사용 불가
        // System.out.println(this.instanceVar);  // 컴파일 에러!

        // ✅ 다른 static 메서드 호출 가능
        anotherStaticMethod();

        // ❌ 인스턴스 메서드 직접 호출 불가
        // instanceMethod();  // 컴파일 에러!

        // ✅ 객체를 생성하면 호출 가능
        MyClass obj = new MyClass();
        obj.instanceMethod();
    }

    public static void anotherStaticMethod() {
        System.out.println("Another static method");
    }

    // 인스턴스 메서드
    public void instanceMethod() {
        // ✅ 모든 것에 접근 가능
        System.out.println(staticVar);
        System.out.println(instanceVar);
        staticMethod();
        anotherStaticMethod();
    }
}

왜 이런 제약이 있을까?

// 이유: static 메서드는 객체 없이 호출 가능
MyClass.staticMethod();  // OK

// 만약 static 메서드에서 인스턴스 변수에 접근한다면?
public static void staticMethod() {
    System.out.println(instanceVar);  // 어떤 객체의 instanceVar?
}
// 객체가 없는데 인스턴스 변수에 접근할 수 없음!

유틸리티 클래스 패턴

public final class StringUtils {
    // 인스턴스화 방지
    private StringUtils() {
        throw new AssertionError("Utility class cannot be instantiated");
    }

    // 모두 static 메서드
    public static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }

    public static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    public static String capitalize(String str) {
        if (isEmpty(str)) {
            return str;
        }
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    public static String reverse(String str) {
        if (str == null) {
            return null;
        }
        return new StringBuilder(str).reverse().toString();
    }
}

// 사용
String result = StringUtils.capitalize("hello");  // "Hello"
boolean blank = StringUtils.isBlank("   ");       // true

싱글톤 패턴과 static

고전적 싱글톤 (Eager Initialization)

public class EagerSingleton {
    // 클래스 로딩 시점에 인스턴스 생성
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {
        // private 생성자로 외부 생성 차단
        System.out.println("EagerSingleton instance created");
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

// 사용
EagerSingleton singleton = EagerSingleton.getInstance();

지연 초기화 싱글톤 (Lazy Initialization)

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
        System.out.println("LazySingleton instance created");
    }

    // ❌ 멀티스레드 환경에서 안전하지 않음
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

Thread-Safe 싱글톤 (Synchronized)

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

    // ✅ 동기화로 Thread-Safe 보장
    // ❌ 성능 저하 (매번 동기화)
    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

Double-Checked Locking (권장)

public class DoubleCheckedSingleton {
    // volatile: 메모리 가시성 보장
    private static volatile DoubleCheckedSingleton instance;

    private DoubleCheckedSingleton() {}

    public static DoubleCheckedSingleton getInstance() {
        // 첫 번째 체크 (동기화 없이)
        if (instance == null) {
            synchronized (DoubleCheckedSingleton.class) {
                // 두 번째 체크 (동기화 블록 내부)
                if (instance == null) {
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }
}

Bill Pugh 싱글톤 (최고의 방법)

public class BillPughSingleton {
    private BillPughSingleton() {}

    // static 내부 클래스: getInstance() 호출 시에만 로드됨
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Enum 싱글톤 (가장 안전한 방법)

public enum EnumSingleton {
    INSTANCE;

    // 인스턴스 변수
    private int value;

    // 메서드
    public void doSomething() {
        System.out.println("EnumSingleton value: " + value);
    }

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

// 사용
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.setValue(42);
singleton.doSomething();

// 장점:
// 1. Thread-Safe 자동 보장
// 2. Serialization 안전
// 3. Reflection 공격 방어
// 4. 간결한 코드

실무 싱글톤 예제: 로깅

public class Logger {
    private static volatile Logger instance;
    private final String logFilePath;

    private Logger() {
        this.logFilePath = "application.log";
        // 로그 파일 초기화
    }

    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
        );
        System.out.println(logEntry);
        // 파일에 쓰기 로직...
    }

    public void info(String message) {
        log("INFO", message);
    }

    public void error(String message) {
        log("ERROR", message);
    }
}

// 사용 - 어디서든 같은 Logger 인스턴스
Logger.getInstance().info("Application started");
Logger.getInstance().error("An error occurred");

static 초기화 블록

static 블록의 실행 순서

public class InitializationOrder {
    // 1. static 변수 선언 및 초기화
    private static int staticVar = initStaticVar();

    // 2. static 초기화 블록
    static {
        System.out.println("Static block 1 executed");
        staticVar += 10;
    }

    // 3. 또 다른 static 블록 (순서대로 실행)
    static {
        System.out.println("Static block 2 executed");
        staticVar += 20;
    }

    // 4. 인스턴스 변수 초기화
    private int instanceVar = initInstanceVar();

    // 5. 인스턴스 초기화 블록
    {
        System.out.println("Instance block executed");
        instanceVar += 10;
    }

    // 6. 생성자
    public InitializationOrder() {
        System.out.println("Constructor executed");
        instanceVar += 20;
    }

    private static int initStaticVar() {
        System.out.println("Static variable initialized");
        return 100;
    }

    private int initInstanceVar() {
        System.out.println("Instance variable initialized");
        return 50;
    }

    public void printValues() {
        System.out.println("staticVar: " + staticVar + ", instanceVar: " + instanceVar);
    }
}

// 실행
InitializationOrder obj1 = new InitializationOrder();
// 출력:
// Static variable initialized
// Static block 1 executed
// Static block 2 executed
// Instance variable initialized
// Instance block executed
// Constructor executed

InitializationOrder obj2 = new InitializationOrder();
// 출력: (static 블록은 실행 안됨!)
// Instance variable initialized
// Instance block executed
// Constructor executed

실무 활용: 복잡한 static 초기화

public class DatabaseDriverManager {
    private static final Map<String, String> DRIVER_MAP = new HashMap<>();
    private static final Set<String> LOADED_DRIVERS = new HashSet<>();

    // static 초기화 블록으로 드라이버 등록
    static {
        try {
            // JDBC 드라이버 등록
            DRIVER_MAP.put("mysql", "com.mysql.cj.jdbc.Driver");
            DRIVER_MAP.put("postgresql", "org.postgresql.Driver");
            DRIVER_MAP.put("h2", "org.h2.Driver");
            DRIVER_MAP.put("oracle", "oracle.jdbc.OracleDriver");

            // 드라이버 로드
            for (Map.Entry<String, String> entry : DRIVER_MAP.entrySet()) {
                try {
                    Class.forName(entry.getValue());
                    LOADED_DRIVERS.add(entry.getKey());
                    System.out.println("Loaded driver: " + entry.getKey());
                } catch (ClassNotFoundException e) {
                    System.err.println("Failed to load driver: " + entry.getKey());
                }
            }

            System.out.println("Total drivers loaded: " + LOADED_DRIVERS.size());

        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private DatabaseDriverManager() {}

    public static boolean isDriverLoaded(String driverType) {
        return LOADED_DRIVERS.contains(driverType);
    }

    public static String getDriverClassName(String driverType) {
        return DRIVER_MAP.get(driverType);
    }
}

static import 활용

// MathConstants.java
public class MathConstants {
    public static final double PI = 3.14159265359;
    public static final double E = 2.71828182846;
    public static final double GOLDEN_RATIO = 1.61803398875;

    public static double square(double x) {
        return x * x;
    }

    public static double cube(double x) {
        return x * x * x;
    }
}

// 사용 파일
import static com.example.MathConstants.*;

public class Calculator {
    public double calculateCircleArea(double radius) {
        // static import 덕분에 클래스명 생략 가능
        return PI * square(radius);
    }

    public double calculateGoldenRectangleArea(double side) {
        return side * side * GOLDEN_RATIO;
    }
}

2.2 상속과 다형성

상속보다 조합(Composition over Inheritance)

상속의 문제점

문제 1: 강한 결합도

// ❌ 나쁜 예: 상속으로 인한 강한 결합
class Employee {
    protected String name;
    protected String department;
    protected double salary;

    public void work() {
        System.out.println(name + " is working");
    }

    public void calculateBonus() {
        System.out.println("Bonus: " + salary * 0.1);
    }
}

class Manager extends Employee {
    private List<Employee> team;

    @Override
    public void calculateBonus() {
        // 부모 클래스의 salary 필드에 의존
        System.out.println("Manager bonus: " + salary * 0.2 + team.size() * 1000);
    }
}

// 문제 발생!
// Employee 클래스의 salary 필드 이름이 변경되면?
// Employee 클래스의 calculateBonus() 로직이 변경되면?
// Manager 클래스도 영향을 받음!

문제 2: 불필요한 메서드 상속

// ❌ Stack은 Vector를 상속 (Java 초기 설계 실수)
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);

// Stack이지만 Vector의 메서드도 사용 가능 (LIFO 원칙 위반!)
stack.add(0, 999);  // 중간에 삽입 가능
stack.remove(1);    // 중간 삭제 가능
// Stack의 의미가 무너짐!

문제 3: 다중 상속 불가

// ❌ Java는 다중 상속 불가
class FlyingCar extends Car, Airplane {  // 컴파일 에러!
    // Car와 Airplane의 기능을 모두 원함
}

문제 4: 깨지기 쉬운 기반 클래스 (Fragile Base Class)

class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);  // 문제 발생!
    }

    public int getAddCount() {
        return addCount;
    }
}

// 사용
InstrumentedHashSet<String> set = new InstrumentedHashSet<>();
set.addAll(Arrays.asList("A", "B", "C"));

System.out.println(set.getAddCount());  // 예상: 3, 실제: 6!
// 이유: HashSet.addAll()이 내부적으로 add()를 호출함
// addAll()에서 3 증가 + add() 3번 호출로 3 증가 = 6

조합을 사용한 설계

해결책: Composition + Delegation

// ✅ 조합 사용
class Employee {
    private String name;
    private String department;
    private Salary salary;  // Has-A 관계

    public Employee(String name, String department, Salary salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public void work() {
        System.out.println(name + " is working");
    }

    public double calculateBonus() {
        return salary.calculateBonus();
    }

    public String getName() { return name; }
    public Salary getSalary() { return salary; }
}

// 급여 계산 로직 분리
class Salary {
    private double baseSalary;

    public Salary(double baseSalary) {
        this.baseSalary = baseSalary;
    }

    public double calculateBonus() {
        return baseSalary * 0.1;
    }

    public double getBaseSalary() {
        return baseSalary;
    }
}

// Manager는 별도 클래스 + 조합
class Manager {
    private Employee employee;  // Employee 조합
    private List<Employee> team;
    private Salary managerSalary;

    public Manager(String name, String department, Salary salary) {
        this.employee = new Employee(name, department, salary);
        this.managerSalary = salary;
        this.team = new ArrayList<>();
    }

    // Delegation: Employee의 기능 위임
    public void work() {
        employee.work();
        System.out.println("Managing team of " + team.size());
    }

    // 고유 기능
    public double calculateBonus() {
        return managerSalary.getBaseSalary() * 0.2 + team.size() * 1000;
    }

    public void addTeamMember(Employee emp) {
        team.add(emp);
    }
}

깨지지 않는 Set 구현

// ✅ Forwarding 클래스
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;  // 조합

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    // 모든 메서드를 위임
    @Override public boolean add(E e) { return s.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
    @Override public boolean remove(Object o) { return s.remove(o); }
    @Override public boolean contains(Object o) { return s.contains(o); }
    @Override public int size() { return s.size(); }
    @Override public boolean isEmpty() { return s.isEmpty(); }
    @Override public void clear() { s.clear(); }
    // ... 나머지 메서드들
}

// ✅ Wrapper 클래스
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// 사용 - 정확하게 동작!
Set<String> set = new InstrumentedSet<>(new HashSet<>());
set.addAll(Arrays.asList("A", "B", "C"));
System.out.println(((InstrumentedSet<String>) set).getAddCount());  // 3

실전 예제: 다양한 능력의 조합

// ❌ 상속으로 구현하면 복잡해짐
interface Movable {
    void move();
}

interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// ✅ 조합으로 구현
class Duck {
    private final Movable walkBehavior;
    private final Flyable flyBehavior;
    private final Swimmable swimBehavior;

    public Duck(Movable walk, Flyable fly, Swimmable swim) {
        this.walkBehavior = walk;
        this.flyBehavior = fly;
        this.swimBehavior = swim;
    }

    public void performWalk() {
        walkBehavior.move();
    }

    public void performFly() {
        flyBehavior.fly();
    }

    public void performSwim() {
        swimBehavior.swim();
    }
}

// 구체적 행동 구현
class WalkOnGround implements Movable {
    @Override
    public void move() {
        System.out.println("Walking on ground");
    }
}

class FlyWithWings implements Flyable {
    @Override
    public void fly() {
        System.out.println("Flying with wings");
    }
}

class SwimWithLegs implements Swimmable {
    @Override
    public void swim() {
        System.out.println("Swimming with legs");
    }
}

class NoFly implements Flyable {
    @Override
    public void fly() {
        System.out.println("Cannot fly");
    }
}

// 사용 - 유연한 조합
Duck mallard = new Duck(
    new WalkOnGround(),
    new FlyWithWings(),
    new SwimWithLegs()
);

Duck rubberDuck = new Duck(
    new WalkOnGround(),
    new NoFly(),  // 고무 오리는 못 날아감
    new SwimWithLegs()
);

mallard.performFly();     // Flying with wings
rubberDuck.performFly();  // Cannot fly

언제 상속을 사용할까?

// ✅ 상속을 사용해도 좋은 경우

// 1. 진정한 "is-a" 관계
class Animal {
    void breathe() { }
}

class Dog extends Animal {  // Dog IS-A Animal
    void bark() { }
}

// 2. 리스코프 치환 원칙(LSP)을 만족
// 부모를 자식으로 대체해도 프로그램이 정상 동작
List<String> list = new ArrayList<>();  // ArrayList IS-A List
list = new LinkedList<>();              // LinkedList IS-A List

// 3. 부모 클래스가 확장을 고려하여 설계됨
abstract class HttpServlet {
    // 템플릿 메서드 패턴
    public final void service(Request req, Response res) {
        doGet(req, res);  // 하위 클래스에서 구현
    }

    protected abstract void doGet(Request req, Response res);
}

class MyServlet extends HttpServlet {
    @Override
    protected void doGet(Request req, Response res) {
        // 구현
    }
}

조합 vs 상속 의사결정 트리

/**
 * 상속을 사용하기 전 체크리스트:
 * 
 * 1. 진정한 "is-a" 관계인가?
 *    ├─ Yes → 2번으로
 *    └─ No → 조합 사용
 * 
 * 2. 부모 클래스의 모든 메서드가 자식에게 적합한가?
 *    ├─ Yes → 3번으로
 *    └─ No → 조합 사용
 * 
 * 3. 부모 클래스가 문서화되고 확장을 고려했는가?
 *    ├─ Yes → 4번으로
 *    └─ No → 조합 사용
 * 
 * 4. 같은 패키지 내에서 사용하는가?
 *    ├─ Yes → 상속 고려 가능
 *    └─ No → 조합 사용 권장
 * 
 * 5. 자식 클래스가 부모의 구현에 의존하지 않는가?
 *    ├─ Yes → 상속 사용 가능
 *    └─ No → 조합 사용
 */

다형성의 힘: 업캐스팅과 다운캐스팅

동적 바인딩(Dynamic Binding)

컴파일 타임 vs 런타임

class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }

    public void eat() {
        System.out.println("Animal eating");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark!");
    }

    public void wagTail() {
        System.out.println("Wagging tail");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }

    public void scratch() {
        System.out.println("Scratching");
    }
}

// 다형성 활용
Animal animal1 = new Dog();  // 업캐스팅
Animal animal2 = new Cat();

// 런타임에 실제 객체의 메서드 호출 (동적 바인딩)
animal1.makeSound();  // "Bark!" (Dog의 메서드)
animal2.makeSound();  // "Meow!" (Cat의 메서드)

// ❌ 컴파일 에러: Animal 타입에는 wagTail() 없음
// animal1.wagTail();

// 다운캐스팅 필요
if (animal1 instanceof Dog) {
    Dog dog = (Dog) animal1;
    dog.wagTail();  // OK
}

바이트코드 레벨에서의 동작

// 메서드 호출 바이트코드
animal1.makeSound();

// 컴파일된 바이트코드:
// invokevirtual #2  // Method Animal.makeSound:()V
// 
// 런타임에:
// 1. animal1의 실제 타입 확인 (Dog)
// 2. Dog 클래스의 메서드 테이블 참조
// 3. Dog.makeSound() 호출

실무 활용: 전략 패턴

// 결제 인터페이스
interface PaymentStrategy {
    void pay(int amount);
}

// 구체적 전략들
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using credit card: " + cardNumber);
    }
}

class KakaoPayPayment implements PaymentStrategy {
    private String phoneNumber;

    public KakaoPayPayment(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using KakaoPay: " + phoneNumber);
    }
}

class BankTransferPayment implements PaymentStrategy {
    private String accountNumber;

    public BankTransferPayment(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using bank transfer: " + accountNumber);
    }
}

// 컨텍스트 클래스
class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    private PaymentStrategy paymentStrategy;

    public void addItem(Item item) {
        items.add(item);
    }

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout() {
        int total = items.stream()
            .mapToInt(Item::getPrice)
            .sum();

        paymentStrategy.pay(total);  // 다형성!
    }
}

// 사용 - 런타임에 전략 변경 가능
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("Book", 10000));
cart.addItem(new Item("Pen", 1000));

// 신용카드로 결제
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout();

// 결제 방식 변경
cart.setPaymentStrategy(new KakaoPayPayment("010-1234-5678"));
cart.checkout();

instanceof와 타입 체크

전통적인 instanceof 사용

public void handleAnimal(Animal animal) {
    // ❌ 타입 체크 후 캐스팅 (번거로움)
    if (animal instanceof Dog) {
        Dog dog = (Dog) animal;
        dog.wagTail();
    } else if (animal instanceof Cat) {
        Cat cat = (Cat) animal;
        cat.scratch();
    } else {
        animal.makeSound();
    }
}

Java 16+ Pattern Matching for instanceof

// ✅ 타입 체크와 캐스팅을 동시에
public void handleAnimal(Animal animal) {
    if (animal instanceof Dog dog) {
        // dog 변수가 자동으로 생성됨!
        dog.wagTail();
    } else if (animal instanceof Cat cat) {
        cat.scratch();
    } else {
        animal.makeSound();
    }
}

// ✅ 더 복잡한 조건과 결합
public String getAnimalInfo(Animal animal) {
    if (animal instanceof Dog dog && dog.getAge() > 5) {
        return "Old dog: " + dog.getName();
    } else if (animal instanceof Cat cat && cat.isIndoor()) {
        return "Indoor cat: " + cat.getName();
    }
    return "Unknown animal";
}

Java 17+ Pattern Matching in switch (Preview)

// ✅ switch에서 패턴 매칭
public String getAnimalSound(Animal animal) {
    return switch (animal) {
        case Dog dog -> {
            dog.wagTail();
            yield "Bark!";
        }
        case Cat cat -> {
            cat.scratch();
            yield "Meow!";
        }
        case null -> "No animal";
        default -> "Unknown sound";
    };
}

// ✅ 가드 조건 추가
public int getAnimalPoints(Animal animal) {
    return switch (animal) {
        case Dog dog when dog.getAge() < 2 -> 100;    // 어린 강아지
        case Dog dog when dog.getAge() >= 2 -> 50;    // 성견
        case Cat cat when cat.isIndoor() -> 80;       // 실내 고양이
        case Cat cat -> 60;                            // 일반 고양이
        default -> 0;
    };
}

instanceof의 안티패턴

// ❌ 나쁜 예: instanceof 남발
public double calculateArea(Shape shape) {
    if (shape instanceof Circle) {
        Circle circle = (Circle) shape;
        return Math.PI * circle.getRadius() * circle.getRadius();
    } else if (shape instanceof Rectangle) {
        Rectangle rect = (Rectangle) shape;
        return rect.getWidth() * rect.getHeight();
    } else if (shape instanceof Triangle) {
        Triangle tri = (Triangle) shape;
        return 0.5 * tri.getBase() * tri.getHeight();
    }
    return 0;
}

// ✅ 좋은 예: 다형성 활용
abstract class Shape {
    abstract double calculateArea();
}

class Circle extends Shape {
    private double radius;

    @Override
    double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    private double width;
    private double height;

    @Override
    double calculateArea() {
        return width * height;
    }
}

// 사용 - instanceof 불필요!
Shape shape = getShape();
double area = shape.calculateArea();  // 다형성으로 해결

instanceof 사용이 적절한 경우

// ✅ equals() 구현
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof User other)) return false;

    return Objects.equals(this.id, other.id) &&
           Objects.equals(this.email, other.email);
}

// ✅ 방문자 패턴(Visitor Pattern)
interface ShapeVisitor {
    void visit(Circle circle);
    void visit(Rectangle rectangle);
}

// ✅ 디버깅/로깅
public void logObject(Object obj) {
    if (obj instanceof String s) {
        log.debug("String value: {}", s);
    } else if (obj instanceof Number n) {
        log.debug("Number value: {}", n);
    } else if (obj instanceof Collection<?> c) {
        log.debug("Collection size: {}", c.size());
    } else {
        log.debug("Object: {}", obj.toString());
    }
}

업캐스팅과 다운캐스팅 베스트 프랙티스

public class CastingBestPractices {
    // ✅ 업캐스팅 (안전, 자동)
    public void upcasting() {
        Dog dog = new Dog();
        Animal animal = dog;  // 자동 업캐스팅
        animal.makeSound();   // Dog의 makeSound() 호출
    }

    // ✅ 다운캐스팅 (위험, 명시적)
    public void downcasting(Animal animal) {
        // 1. instanceof로 먼저 확인
        if (animal instanceof Dog dog) {
            dog.wagTail();  // 안전
        }
    }

    // ❌ 위험한 다운캐스팅
    public void unsafeDowncasting(Animal animal) {
        Dog dog = (Dog) animal;  // ClassCastException 위험!
        dog.wagTail();
    }

    // ✅ Optional을 사용한 안전한 캐스팅
    public Optional<Dog> safeCast(Animal animal) {
        if (animal instanceof Dog dog) {
            return Optional.of(dog);
        }
        return Optional.empty();
    }

    // 사용
    public void useSafeCast(Animal animal) {
        safeCast(animal).ifPresent(Dog::wagTail);
    }
}

2.3 추상 클래스와 인터페이스

추상 클래스 vs 인터페이스: 언제 무엇을 쓸까?

기본 차이점

추상 클래스 (Abstract Class)

public abstract class Vehicle {
    // 인스턴스 변수 가능
    protected String brand;
    protected int year;

    // 생성자 가능
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }

    // 구현된 메서드 가능
    public void displayInfo() {
        System.out.println(brand + " (" + year + ")");
    }

    // 추상 메서드 (구현 강제)
    public abstract void start();
    public abstract void stop();

    // protected 메서드
    protected void checkMaintenance() {
        System.out.println("Checking maintenance...");
    }
}

class Car extends Vehicle {
    private int doors;

    public Car(String brand, int year, int doors) {
        super(brand, year);  // 부모 생성자 호출 필수
        this.doors = doors;
    }

    @Override
    public void start() {
        System.out.println("Car engine started");
    }

    @Override
    public void stop() {
        System.out.println("Car engine stopped");
    }
}

인터페이스 (Interface)

public interface Drivable {
    // 상수만 가능 (public static final 자동)
    int MAX_SPEED = 200;

    // 추상 메서드 (public abstract 자동)
    void accelerate();
    void brake();

    // Java 8+: default 메서드
    default void honk() {
        System.out.println("Beep beep!");
    }

    // Java 8+: static 메서드
    static void checkLicense(String license) {
        System.out.println("Checking license: " + license);
    }

    // Java 9+: private 메서드
    private void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

class SportsCar implements Drivable {
    @Override
    public void accelerate() {
        System.out.println("Accelerating fast!");
    }

    @Override
    public void brake() {
        System.out.println("Braking hard!");
    }

    // honk()는 오버라이드 안해도 됨 (default 메서드)
}

비교표

특징 추상 클래스 인터페이스
다중 상속 ❌ (단일 상속만) ✅ (다중 구현)
인스턴스 변수 ❌ (상수만)
생성자
접근 제어자 모두 가능 public만 (Java 9+ private)
default 메서드 - ✅ (Java 8+)
static 메서드 ✅ (Java 8+)
사용 목적 공통 기능 + 상태 계약(Contract) 정의

Java 8 이후 인터페이스의 변화

default 메서드

public interface List<E> {
    // 기존 메서드들
    boolean add(E e);
    E get(int index);

    // Java 8+: default 메서드 (기존 구현체 깨지지 않음!)
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

// 사용
List<String> list = new ArrayList<>();
list.add("C");
list.add("A");
list.add("B");
list.sort(Comparator.naturalOrder());  // default 메서드!

static 메서드

public interface Comparator<T> {
    int compare(T o1, T o2);

    // Java 8+: static 팩토리 메서드
    static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }

    static <T> Comparator<T> reverseOrder() {
        return Collections.reverseOrder();
    }

    static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor) {
        return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }
}

// 사용
List<User> users = getUsers();
users.sort(Comparator.comparing(User::getName));  // static 메서드!

private 메서드 (Java 9+)

public interface Calculator {
    default int addAll(int... numbers) {
        return calculate(numbers, (a, b) -> a + b);
    }

    default int multiplyAll(int... numbers) {
        return calculate(numbers, (a, b) -> a * b);
    }

    // private 메서드로 중복 코드 제거
    private int calculate(int[] numbers, IntBinaryOperator op) {
        if (numbers.length == 0) return 0;
        int result = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            result = op.applyAsInt(result, numbers[i]);
        }
        return result;
    }
}

다중 상속 문제 해결

Diamond Problem

interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}

interface B extends A {
    default void hello() {
        System.out.println("Hello from B");
    }
}

interface C extends A {
    default void hello() {
        System.out.println("Hello from C");
    }
}

// ❌ Diamond Problem 발생!
class D implements B, C {
    // 컴파일 에러: 어떤 hello()를 사용할지 모호함
}

// ✅ 해결책: 명시적으로 오버라이드
class D implements B, C {
    @Override
    public void hello() {
        // 1. 특정 인터페이스의 메서드 명시
        B.super.hello();

        // 2. 자신만의 구현
        System.out.println("Hello from D");

        // 3. 두 메서드 모두 호출
        B.super.hello();
        C.super.hello();
    }
}

실무 예제: 다중 역할

// 여러 역할을 인터페이스로 정의
interface Readable {
    String read();

    default void displayContent() {
        System.out.println("Content: " + read());
    }
}

interface Writable {
    void write(String content);

    default void writeAndLog(String content) {
        System.out.println("Writing: " + content);
        write(content);
    }
}

interface Closeable {
    void close();

    default void closeWithLog() {
        System.out.println("Closing...");
        close();
    }
}

// 다중 구현으로 여러 역할 수행
class File implements Readable, Writable, Closeable {
    private String content = "";
    private boolean closed = false;

    @Override
    public String read() {
        if (closed) throw new IllegalStateException("File is closed");
        return content;
    }

    @Override
    public void write(String content) {
        if (closed) throw new IllegalStateException("File is closed");
        this.content += content;
    }

    @Override
    public void close() {
        closed = true;
    }
}

// 사용 - 다형성으로 역할 선택
File file = new File();
file.write("Hello");

Readable readable = file;
System.out.println(readable.read());

Closeable closeable = file;
closeable.closeWithLog();

언제 무엇을 사용할까?

추상 클래스를 사용하는 경우

// ✅ 공통 상태와 동작이 있는 경우
public abstract class HttpServlet {
    // 공통 상태
    protected ServletConfig config;

    // 생성자로 초기화
    public void init(ServletConfig config) {
        this.config = config;
    }

    // 공통 동작 (템플릿 메서드 패턴)
    public final void service(HttpRequest req, HttpResponse res) {
        String method = req.getMethod();

        if ("GET".equals(method)) {
            doGet(req, res);
        } else if ("POST".equals(method)) {
            doPost(req, res);
        }
    }

    // 하위 클래스에서 구현
    protected abstract void doGet(HttpRequest req, HttpResponse res);
    protected abstract void doPost(HttpRequest req, HttpResponse res);
}

class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpRequest req, HttpResponse res) {
        // GET 요청 처리
    }

    @Override
    protected void doPost(HttpRequest req, HttpResponse res) {
        // POST 요청 처리
    }
}

인터페이스를 사용하는 경우

// ✅ 계약(Contract) 정의
public interface UserRepository {
    User findById(Long id);
    List<User> findAll();
    void save(User user);
    void delete(Long id);
}

// 다양한 구현체
class JpaUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // JPA로 구현
        return entityManager.find(User.class, id);
    }
    // ...
}

class MongoUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // MongoDB로 구현
        return mongoTemplate.findById(id, User.class);
    }
    // ...
}

// ✅ 서비스 계층은 인터페이스에만 의존
class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;  // 구현체 몰라도 됨!
    }

    public User getUser(Long id) {
        return repository.findById(id);
    }
}

실무 의사결정 가이드

/**
 * 추상 클래스 선택:
 * 1. "is-a" 관계가 명확함
 * 2. 공통 상태(필드)가 필요함
 * 3. protected 멤버 필요
 * 4. 생성자가 필요함
 * 5. 코드 중복을 줄이고 싶음
 * 
 * 예: BaseEntity, AbstractController
 */

public abstract class BaseEntity {
    protected Long id;
    protected LocalDateTime createdAt;
    protected LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

/**
 * 인터페이스 선택:
 * 1. 다중 구현이 필요함
 * 2. 계약만 정의하면 됨
 * 3. 다양한 구현체가 예상됨
 * 4. 테스트 용이성 (Mock)
 * 5. API/SPI 정의
 * 
 * 예: Repository, Service, Strategy
 */

public interface NotificationService {
    void send(String to, String message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void send(String to, String message) {
        // 이메일 전송
    }
}

class SmsNotificationService implements NotificationService {
    @Override
    public void send(String to, String message) {
        // SMS 전송
    }
}

함수형 인터페이스와 람다 표현식 입문

@FunctionalInterface

함수형 인터페이스란?

// 추상 메서드가 정확히 1개인 인터페이스
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);

    // default 메서드는 여러 개 가능
    default int add(int a, int b) {
        return a + b;
    }

    // static 메서드도 여러 개 가능
    static int multiply(int a, int b) {
        return a * b;
    }
}

// ❌ 컴파일 에러: 추상 메서드가 2개
@FunctionalInterface
public interface Invalid {
    void method1();
    void method2();  // 에러!
}

Java 기본 함수형 인터페이스

// 1. Function<T, R>: T를 받아서 R 반환
Function<String, Integer> strLength = s -> s.length();
int len = strLength.apply("Hello");  // 5

// 2. Predicate<T>: T를 받아서 boolean 반환
Predicate<Integer> isEven = n -> n % 2 == 0;
boolean result = isEven.test(4);  // true

// 3. Consumer<T>: T를 받아서 처리 (반환값 없음)
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello");  // 출력: Hello

// 4. Supplier<T>: 파라미터 없이 T 반환
Supplier<Double> randomSupplier = () -> Math.random();
double random = randomSupplier.get();

// 5. BiFunction<T, U, R>: T, U를 받아서 R 반환
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(10, 20);  // 30

// 6. UnaryOperator<T>: T를 받아서 T 반환
UnaryOperator<Integer> square = n -> n * n;
int squared = square.apply(5);  // 25

// 7. BinaryOperator<T>: T, T를 받아서 T 반환
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
int maxValue = max.apply(10, 20);  // 20

람다 표현식 기초

문법

// 기본 형식: (parameters) -> expression
// 또는: (parameters) -> { statements; }

// 파라미터 없음
() -> System.out.println("Hello")
() -> 42
() -> { return 42; }

// 파라미터 1개 (괄호 생략 가능)
x -> x * x
(x) -> x * x
x -> { return x * x; }

// 파라미터 2개 이상
(x, y) -> x + y
(x, y) -> { return x + y; }

// 타입 명시 가능
(int x, int y) -> x + y
(String s) -> s.length()

익명 클래스 vs 람다

// ❌ 익명 클래스 (장황함)
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running...");
    }
};

// ✅ 람다 (간결함)
Runnable runnable2 = () -> System.out.println("Running...");

// ❌ 익명 클래스
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

// ✅ 람다
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

// ✅✅ 메서드 레퍼런스 (더 간결)
Collections.sort(names, String::compareTo);

실전 예제

public class LambdaExamples {
    // 필터링
    public List<User> filterActiveUsers(List<User> users) {
        return users.stream()
            .filter(user -> user.isActive())
            .collect(Collectors.toList());
    }

    // 변환
    public List<String> getUserNames(List<User> users) {
        return users.stream()
            .map(user -> user.getName())
            .collect(Collectors.toList());
    }

    // 정렬
    public List<User> sortByAge(List<User> users) {
        return users.stream()
            .sorted((u1, u2) -> Integer.compare(u1.getAge(), u2.getAge()))
            .collect(Collectors.toList());
    }

    // 집계
    public int getTotalAge(List<User> users) {
        return users.stream()
            .mapToInt(user -> user.getAge())
            .sum();
    }

    // 그룹화
    public Map<String, List<User>> groupByDepartment(List<User> users) {
        return users.stream()
            .collect(Collectors.groupingBy(user -> user.getDepartment()));
    }
}

메서드 레퍼런스

4가지 유형

// 1. 정적 메서드 레퍼런스: ClassName::staticMethod
Function<String, Integer> parseInt1 = s -> Integer.parseInt(s);
Function<String, Integer> parseInt2 = Integer::parseInt;  // 동일

// 2. 인스턴스 메서드 레퍼런스: instance::instanceMethod
String str = "Hello";
Supplier<Integer> getLength1 = () -> str.length();
Supplier<Integer> getLength2 = str::length;  // 동일

// 3. 특정 타입의 인스턴스 메서드 레퍼런스: ClassName::instanceMethod
Function<String, Integer> getLength3 = s -> s.length();
Function<String, Integer> getLength4 = String::length;  // 동일

// 4. 생성자 레퍼런스: ClassName::new
Supplier<List<String>> listSupplier1 = () -> new ArrayList<>();
Supplier<List<String>> listSupplier2 = ArrayList::new;  // 동일

Function<String, User> userCreator1 = name -> new User(name);
Function<String, User> userCreator2 = User::new;  // 동일

실전 활용

public class MethodReferenceExamples {
    // 정적 메서드 레퍼런스
    public List<Integer> parseNumbers(List<String> strings) {
        return strings.stream()
            .map(Integer::parseInt)  // String -> Integer
            .collect(Collectors.toList());
    }

    // 인스턴스 메서드 레퍼런스
    public void printAll(List<String> items) {
        items.forEach(System.out::println);  // Consumer<String>
    }

    // 특정 타입의 인스턴스 메서드
    public List<String> toUpperCase(List<String> strings) {
        return strings.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
    }

    // 생성자 레퍼런스
    public List<User> createUsers(List<String> names) {
        return names.stream()
            .map(User::new)  // new User(name)
            .collect(Collectors.toList());
    }

    // 배열 생성자 레퍼런스
    public String[] toArray(List<String> list) {
        return list.stream()
            .toArray(String[]::new);  // new String[size]
    }
}

복잡한 메서드 레퍼런스

public class AdvancedMethodReferences {
    // 정렬
    public List<User> sortByName(List<User> users) {
        // ❌ 람다
        return users.stream()
            .sorted((u1, u2) -> u1.getName().compareTo(u2.getName()))
            .collect(Collectors.toList());

        // ✅ 메서드 레퍼런스
        return users.stream()
            .sorted(Comparator.comparing(User::getName))
            .collect(Collectors.toList());
    }

    // 다중 정렬
    public List<User> sortByDepartmentAndAge(List<User> users) {
        return users.stream()
            .sorted(Comparator.comparing(User::getDepartment)
                .thenComparing(User::getAge))
            .collect(Collectors.toList());
    }

    // 역순 정렬
    public List<User> sortByAgeDescending(List<User> users) {
        return users.stream()
            .sorted(Comparator.comparing(User::getAge).reversed())
            .collect(Collectors.toList());
    }

    // null 안전 정렬
    public List<User> sortByNameNullSafe(List<User> users) {
        return users.stream()
            .sorted(Comparator.comparing(User::getName, 
                Comparator.nullsLast(String::compareTo)))
            .collect(Collectors.toList());
    }
}

커스텀 함수형 인터페이스

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

// 사용
TriFunction<Integer, Integer, Integer, Integer> sum3 = 
    (a, b, c) -> a + b + c;

int result = sum3.apply(1, 2, 3);  // 6

@FunctionalInterface
public interface Validator<T> {
    boolean validate(T value);

    default Validator<T> and(Validator<T> other) {
        return value -> this.validate(value) && other.validate(value);
    }

    default Validator<T> or(Validator<T> other) {
        return value -> this.validate(value) || other.validate(value);
    }
}

// 사용
Validator<String> notEmpty = s -> s != null && !s.isEmpty();
Validator<String> minLength = s -> s.length() >= 5;

Validator<String> combinedValidator = notEmpty.and(minLength);
boolean valid = combinedValidator.validate("Hello");  // true

마치며

이번 글에서는 Java 객체지향 프로그래밍의 핵심을 깊이 있게 다뤄보았습니다.

핵심 요약:

  1. 클래스 설계: Builder 패턴, 접근 제어자의 올바른 사용
  2. static 멤버: 싱글톤 패턴, 메모리 관점의 이해
  3. 상속 vs 조합: 유연한 설계를 위한 조합 선호
  4. 다형성: 동적 바인딩, Pattern Matching 활용
  5. 추상화: 추상 클래스와 인터페이스의 적절한 선택
  6. 함수형 프로그래밍: 람다와 메서드 레퍼런스

실무에서는 단순히 문법을 아는 것을 넘어, 왜 그렇게 설계해야 하는지, 어떤 트레이드오프가 있는지를 이해하는 것이 중요합니다.

다음 단계에서는 제네릭, 컬렉션 프레임워크, 예외 처리 등을 다룰 예정입니다.

반응형