Repository

8단계: Java 기능 (Java 17+) 본문

Java

8단계: Java 기능 (Java 17+)

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

8단계: Java 기능 (Java 17+)

4년간의 실무에서 Java 8부터 Java21까지 마이그레이션을 진행하며 겪은 경험을 바탕으로, 단순한 기능 소개가 아닌 '언제', '왜', '어떻게' 사용해야 하는지를 실전 관점에서 다룹니다.


8.1 Java 8-11 주요 기능

Java 8의 게임 체인저들

Lambda & Stream API: 함수형 프로그래밍의 시작

Lambda 표현식의 혁명

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

// ✅ Java 8: Lambda 표현식
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

// ✅✅ 더 간결하게: Method Reference
Collections.sort(names, String::compareTo);

// ✅✅✅ Stream API 활용
List<String> sortedNames = names.stream()
    .sorted()
    .collect(Collectors.toList());

실무에서의 Lambda 활용

public class LambdaRealWorldExamples {

    // 1. 필터링과 변환
    public List<UserDTO> getActiveUsers(List<User> users) {
        return users.stream()
            .filter(User::isActive)
            .filter(user -> user.getAge() >= 18)
            .map(this::convertToDTO)
            .collect(Collectors.toList());
    }

    private UserDTO convertToDTO(User user) {
        return new UserDTO(user.getId(), user.getName(), user.getEmail());
    }

    // 2. 그룹화와 집계
    public Map<String, Long> countUsersByDepartment(List<User> users) {
        return users.stream()
            .collect(Collectors.groupingBy(
                User::getDepartment,
                Collectors.counting()
            ));
    }

    // 3. 복잡한 데이터 변환
    public Map<String, List<String>> getUserEmailsByDepartment(List<User> users) {
        return users.stream()
            .collect(Collectors.groupingBy(
                User::getDepartment,
                Collectors.mapping(
                    User::getEmail,
                    Collectors.toList()
                )
            ));
    }

    // 4. flatMap으로 중첩 구조 펼치기
    public List<String> getAllSkills(List<User> users) {
        return users.stream()
            .map(User::getSkills)  // List<User> → Stream<List<String>>
            .flatMap(List::stream)  // Stream<List<String>> → Stream<String>
            .distinct()
            .sorted()
            .collect(Collectors.toList());
    }

    // 5. 조기 종료 (Short-circuit)
    public Optional<User> findFirstAdminUser(List<User> users) {
        return users.stream()
            .filter(user -> "ADMIN".equals(user.getRole()))
            .findFirst();  // 첫 번째를 찾으면 즉시 종료
    }
}

Optional: null 안전성의 새로운 접근

Optional의 올바른 사용법

public class OptionalBestPractices {

    // ❌ 나쁜 예: Optional.get() 직접 호출
    public String getBadExample(Optional<String> optional) {
        if (optional.isPresent()) {
            return optional.get();  // 의미 없는 Optional 사용
        }
        return "default";
    }

    // ✅ 좋은 예: orElse 사용
    public String getGoodExample(Optional<String> optional) {
        return optional.orElse("default");
    }

    // ✅ orElseGet: 기본값 생성 비용이 클 때
    public User getUserOrDefault(Long userId) {
        return userRepository.findById(userId)
            .orElseGet(() -> createDefaultUser());  // 필요할 때만 호출
    }

    private User createDefaultUser() {
        System.out.println("Creating default user...");
        return new User("guest", "guest@example.com");
    }

    // ✅ orElseThrow: 값이 없으면 예외 발생
    public User getUserOrThrow(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
    }

    // ✅ map: 값이 있을 때만 변환
    public Optional<String> getUserEmail(Long userId) {
        return userRepository.findById(userId)
            .map(User::getEmail);
    }

    // ✅ flatMap: 중첩 Optional 처리
    public Optional<String> getUserDepartmentName(Long userId) {
        return userRepository.findById(userId)
            .flatMap(User::getDepartment)  // Optional<Department>
            .map(Department::getName);      // Optional<String>
    }

    // ✅ filter: 조건부 처리
    public Optional<User> getAdultUser(Long userId) {
        return userRepository.findById(userId)
            .filter(user -> user.getAge() >= 18);
    }

    // ✅ ifPresent: 값이 있을 때만 실행
    public void sendEmailIfUserExists(Long userId) {
        userRepository.findById(userId)
            .ifPresent(user -> emailService.send(user.getEmail(), "Welcome!"));
    }

    // ✅ ifPresentOrElse (Java 9+): 값 유무에 따라 다른 동작
    public void processUser(Long userId) {
        userRepository.findById(userId)
            .ifPresentOrElse(
                user -> System.out.println("Found user: " + user.getName()),
                () -> System.out.println("User not found")
            );
    }
}

Optional 안티패턴

public class OptionalAntiPatterns {

    // ❌ 안티패턴 1: Optional을 필드로 사용
    public class BadUser {
        private Optional<String> nickname;  // 절대 금지!

        // Optional은 직렬화 불가능
        // 메모리 오버헤드 발생
    }

    // ✅ 올바른 방법
    public class GoodUser {
        private String nickname;  // null 허용

        public Optional<String> getNickname() {
            return Optional.ofNullable(nickname);
        }
    }

    // ❌ 안티패턴 2: Optional 파라미터
    public void badMethod(Optional<String> name) {
        // 호출자에게 Optional 생성 부담
    }

    // ✅ 올바른 방법: Overloading 사용
    public void goodMethod(String name) {
        // name이 null일 수 있음을 문서화
    }

    public void goodMethod() {
        goodMethod("default");
    }

    // ❌ 안티패턴 3: Optional 컬렉션
    public Optional<List<User>> badGetUsers() {
        // List 자체가 비어있을 수 있으므로 불필요
        return Optional.of(new ArrayList<>());
    }

    // ✅ 올바른 방법: 빈 컬렉션 반환
    public List<User> goodGetUsers() {
        return Collections.emptyList();  // null 대신 빈 리스트
    }

    // ❌ 안티패턴 4: Optional.of(null)
    public Optional<String> badGetValue(String input) {
        return Optional.of(input);  // input이 null이면 NullPointerException!
    }

    // ✅ 올바른 방법: Optional.ofNullable
    public Optional<String> goodGetValue(String input) {
        return Optional.ofNullable(input);
    }
}

새로운 Date/Time API: java.time 패키지

LocalDate, LocalTime, LocalDateTime

public class DateTimeAPIExamples {

    // ❌ Java 7 이전: Date와 Calendar의 문제점
    public void oldDateAPI() {
        Date date = new Date();  // Mutable (변경 가능)
        date.setYear(2023 - 1900);  // 혼란스러운 API

        Calendar calendar = Calendar.getInstance();
        calendar.set(2023, 0, 1);  // 월이 0부터 시작!
    }

    // ✅ Java 8+: 불변 Date/Time API
    public void newDateTimeAPI() {
        // 날짜
        LocalDate today = LocalDate.now();
        LocalDate specificDate = LocalDate.of(2023, 1, 1);
        LocalDate parsed = LocalDate.parse("2023-01-01");

        // 시간
        LocalTime now = LocalTime.now();
        LocalTime specificTime = LocalTime.of(14, 30, 0);

        // 날짜 + 시간
        LocalDateTime dateTime = LocalDateTime.now();
        LocalDateTime specific = LocalDateTime.of(2023, 1, 1, 14, 30);
    }

    // 날짜 연산
    public void dateCalculations() {
        LocalDate today = LocalDate.now();

        // 더하기/빼기
        LocalDate nextWeek = today.plusWeeks(1);
        LocalDate lastMonth = today.minusMonths(1);
        LocalDate nextYear = today.plusYears(1);

        // 특정 날짜로 변경
        LocalDate firstDayOfMonth = today.withDayOfMonth(1);
        LocalDate endOfYear = today.withMonth(12).withDayOfMonth(31);

        // 날짜 비교
        boolean isBefore = today.isBefore(nextWeek);
        boolean isAfter = today.isAfter(lastMonth);
        boolean isEqual = today.isEqual(LocalDate.now());
    }

    // 기간 계산
    public void periodCalculations() {
        LocalDate start = LocalDate.of(2023, 1, 1);
        LocalDate end = LocalDate.of(2023, 12, 31);

        // Period: 날짜 기반 기간
        Period period = Period.between(start, end);
        System.out.println("Years: " + period.getYears());
        System.out.println("Months: " + period.getMonths());
        System.out.println("Days: " + period.getDays());

        // Duration: 시간 기반 기간
        LocalDateTime startTime = LocalDateTime.of(2023, 1, 1, 9, 0);
        LocalDateTime endTime = LocalDateTime.of(2023, 1, 1, 17, 30);

        Duration duration = Duration.between(startTime, endTime);
        System.out.println("Hours: " + duration.toHours());
        System.out.println("Minutes: " + duration.toMinutes());
    }

    // ZonedDateTime: 시간대 처리
    public void timeZoneHandling() {
        // 현재 시간대
        ZonedDateTime now = ZonedDateTime.now();

        // 특정 시간대
        ZonedDateTime seoulTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
        ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));

        // 시간대 변환
        ZonedDateTime converted = seoulTime.withZoneSameInstant(ZoneId.of("UTC"));

        System.out.println("Seoul: " + seoulTime);
        System.out.println("New York: " + newYorkTime);
        System.out.println("UTC: " + converted);
    }

    // 실무 활용: 날짜 포맷팅
    public void dateFormatting() {
        LocalDateTime now = LocalDateTime.now();

        // 기본 포맷터
        String isoFormat = now.format(DateTimeFormatter.ISO_DATE_TIME);

        // 커스텀 포맷
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String customFormat = now.format(formatter);

        // 파싱
        LocalDateTime parsed = LocalDateTime.parse("2023-01-01 14:30:00", formatter);

        System.out.println("ISO: " + isoFormat);
        System.out.println("Custom: " + customFormat);
    }

    // 실무 활용: 영업일 계산
    public LocalDate addBusinessDays(LocalDate date, int days) {
        LocalDate result = date;
        int addedDays = 0;

        while (addedDays < days) {
            result = result.plusDays(1);
            // 주말 제외
            if (result.getDayOfWeek() != DayOfWeek.SATURDAY && 
                result.getDayOfWeek() != DayOfWeek.SUNDAY) {
                addedDays++;
            }
        }

        return result;
    }

    // 실무 활용: 나이 계산
    public int calculateAge(LocalDate birthDate) {
        return Period.between(birthDate, LocalDate.now()).getYears();
    }
}

CompletableFuture: 비동기 프로그래밍

CompletableFuture 기본 사용법

public class CompletableFutureExamples {

    // 기본 비동기 작업
    public void basicAsyncTasks() {
        // 반환값 없는 비동기 작업
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            System.out.println("Running async task");
            sleep(1000);
        });

        // 반환값 있는 비동기 작업
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "Result";
        });

        // 결과 가져오기 (blocking)
        String result = future2.join();  // 또는 get()
        System.out.println(result);
    }

    // 작업 체이닝
    public void chainingTasks() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Task 1: Fetching user data");
            sleep(1000);
            return "user123";
        }).thenApply(userId -> {
            System.out.println("Task 2: Fetching user details for " + userId);
            sleep(1000);
            return new User(userId, "John Doe");
        }).thenApply(user -> {
            System.out.println("Task 3: Formatting user data");
            return user.getName() + " (" + user.getId() + ")";
        });

        System.out.println("Final result: " + future.join());
    }

    // 실무 활용: 병렬 API 호출
    public UserProfile getUserProfile(String userId) {
        // 3개의 API를 병렬로 호출
        CompletableFuture<User> userFuture = 
            CompletableFuture.supplyAsync(() -> fetchUser(userId));

        CompletableFuture<List<Order>> ordersFuture = 
            CompletableFuture.supplyAsync(() -> fetchOrders(userId));

        CompletableFuture<List<Review>> reviewsFuture = 
            CompletableFuture.supplyAsync(() -> fetchReviews(userId));

        // 모든 작업 완료 대기
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
            userFuture, ordersFuture, reviewsFuture
        );

        // 결과 조합
        return allFutures.thenApply(v -> {
            User user = userFuture.join();
            List<Order> orders = ordersFuture.join();
            List<Review> reviews = reviewsFuture.join();

            return new UserProfile(user, orders, reviews);
        }).join();
    }

    // 예외 처리
    public void exceptionHandling() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() > 0.5) {
                throw new RuntimeException("Random error");
            }
            return "Success";
        }).exceptionally(ex -> {
            System.err.println("Error: " + ex.getMessage());
            return "Default value";
        });

        System.out.println(future.join());
    }

    // 시간 제한
    public void timeoutHandling() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            sleep(5000);  // 5초 걸리는 작업
            return "Completed";
        });

        try {
            // 2초 타임아웃 (Java 9+)
            String result = future.orTimeout(2, TimeUnit.SECONDS).join();
            System.out.println(result);
        } catch (Exception e) {
            System.err.println("Timeout!");
        }
    }

    // 실무 활용: 여러 소스에서 데이터 가져오기
    public Product getProductWithBestPrice(String productId) {
        List<String> suppliers = Arrays.asList("SupplierA", "SupplierB", "SupplierC");

        // 모든 공급업체에 병렬 요청
        List<CompletableFuture<ProductPrice>> futures = suppliers.stream()
            .map(supplier -> CompletableFuture.supplyAsync(() -> 
                fetchPriceFromSupplier(productId, supplier)
            ))
            .collect(Collectors.toList());

        // 가장 먼저 완료된 결과 사용
        CompletableFuture<Object> firstCompleted = CompletableFuture.anyOf(
            futures.toArray(new CompletableFuture[0])
        );

        ProductPrice bestPrice = (ProductPrice) firstCompleted.join();
        System.out.println("Best price: " + bestPrice.getPrice() + 
            " from " + bestPrice.getSupplier());

        return new Product(productId, bestPrice);
    }

    // Helper methods
    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private User fetchUser(String userId) {
        sleep(500);
        return new User(userId, "John Doe");
    }

    private List<Order> fetchOrders(String userId) {
        sleep(500);
        return Arrays.asList(new Order("order1"), new Order("order2"));
    }

    private List<Review> fetchReviews(String userId) {
        sleep(500);
        return Arrays.asList(new Review("Great!"), new Review("Excellent!"));
    }

    private ProductPrice fetchPriceFromSupplier(String productId, String supplier) {
        sleep(1000);
        return new ProductPrice(supplier, Math.random() * 100);
    }
}

Java 9-11 신기능 정리

모듈 시스템 (Jigsaw)

모듈의 필요성

// module-info.java
module com.example.app {
    // 외부에 공개할 패키지
    exports com.example.app.api;

    // 특정 모듈에만 공개
    exports com.example.app.internal to com.example.test;

    // 다른 모듈 의존성
    requires java.sql;
    requires transitive java.logging;

    // 서비스 제공
    provides com.example.app.api.Service 
        with com.example.app.impl.ServiceImpl;
}

실무에서의 모듈 시스템 활용

// 모듈 구조 예제
// core 모듈
module com.myapp.core {
    exports com.myapp.core.api;
    exports com.myapp.core.model;
}

// service 모듈
module com.myapp.service {
    requires com.myapp.core;
    exports com.myapp.service;
}

// web 모듈
module com.myapp.web {
    requires com.myapp.core;
    requires com.myapp.service;
    requires spring.web;
}

var 키워드: 타입 추론

var의 올바른 사용법

public class VarKeywordExamples {

    public void properVarUsage() {
        // ✅ 좋은 예: 명확한 타입
        var name = "John Doe";  // String
        var age = 30;  // int
        var price = 99.99;  // double

        var users = new ArrayList<User>();  // ArrayList<User>
        var map = new HashMap<String, Integer>();  // HashMap<String, Integer>

        // ✅ 좋은 예: 복잡한 제네릭 타입
        var result = new HashMap<String, List<Map<String, Object>>>();
        // 명시적 타입보다 훨씬 간결

        // ✅ 좋은 예: Stream 중간 변수
        var filteredUsers = users.stream()
            .filter(User::isActive)
            .collect(Collectors.toList());
    }

    public void badVarUsage() {
        // ❌ 나쁜 예: 타입이 불명확
        var data = getData();  // 무슨 타입인지 알 수 없음

        // ❌ 나쁜 예: null 초기화 불가능
        // var value = null;  // 컴파일 에러!

        // ❌ 나쁜 예: 배열 초기화
        // var array = {1, 2, 3};  // 컴파일 에러!
        var array = new int[]{1, 2, 3};  // OK

        // ❌ 나쁜 예: 다이아몬드 연산자와 함께 사용
        // var list = new ArrayList<>();  // 타입 추론 불가
        var list = new ArrayList<String>();  // OK
    }

    // ✅ var는 로컬 변수에만 사용 가능
    public void localVariableOnly() {
        var localVar = "OK";  // ✅

        // ❌ 필드, 메서드 파라미터, 반환 타입에는 사용 불가
        // private var field = "NOT OK";
        // public var method(var param) { ... }
    }

    // ✅ 실무 활용: try-with-resources
    public void tryWithResourcesVar() {
        try (var reader = new BufferedReader(new FileReader("file.txt"))) {
            var line = reader.readLine();
            System.out.println(line);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // ✅ 실무 활용: for 루프
    public void forLoopVar() {
        var numbers = List.of(1, 2, 3, 4, 5);

        for (var number : numbers) {
            System.out.println(number);
        }

        for (var i = 0; i < numbers.size(); i++) {
            System.out.println(numbers.get(i));
        }
    }
}

HTTP Client API (Java 11)

새로운 HTTP Client

public class HttpClientExamples {

    private final HttpClient httpClient = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .connectTimeout(Duration.ofSeconds(10))
        .build();

    // 동기 GET 요청
    public String syncGetRequest(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .header("Accept", "application/json")
            .build();

        HttpResponse<String> response = httpClient.send(
            request, 
            HttpResponse.BodyHandlers.ofString()
        );

        System.out.println("Status: " + response.statusCode());
        return response.body();
    }

    // 비동기 GET 요청
    public CompletableFuture<String> asyncGetRequest(String url) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body);
    }

    // POST 요청
    public String postRequest(String url, String jsonBody) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
            .header("Content-Type", "application/json")
            .build();

        HttpResponse<String> response = httpClient.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        return response.body();
    }

    // 실무 활용: 병렬 API 호출
    public Map<String, String> fetchMultipleUrls(List<String> urls) {
        List<CompletableFuture<Map.Entry<String, String>>> futures = urls.stream()
            .map(url -> asyncGetRequest(url)
                .thenApply(body -> Map.entry(url, body))
            )
            .collect(Collectors.toList());

        return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }
}

유용한 String 메서드 (Java 11)

public class Java11StringMethods {

    public void newStringMethods() {
        // isBlank(): 빈 문자열 또는 공백만 있는지 확인
        String blank = "   ";
        System.out.println(blank.isBlank());  // true

        // lines(): 줄 단위로 분리
        String multiline = "Line 1\nLine 2\nLine 3";
        multiline.lines().forEach(System.out::println);

        // strip(), stripLeading(), stripTrailing(): 공백 제거
        String text = "  Hello World  ";
        System.out.println(text.strip());  // "Hello World"
        System.out.println(text.stripLeading());  // "Hello World  "
        System.out.println(text.stripTrailing());  // "  Hello World"

        // repeat(): 문자열 반복
        String star = "*";
        System.out.println(star.repeat(10));  // "**********"
    }
}

8.2 Java 17 LTS

Java 17의 새로운 기능들

Records: 간결한 데이터 클래스

Record의 기본 사용법

// ❌ Java 16 이전: 보일러플레이트 코드
public class OldUser {
    private final String name;
    private final String email;
    private final int age;

    public OldUser(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OldUser oldUser = (OldUser) o;
        return age == oldUser.age && 
               Objects.equals(name, oldUser.name) && 
               Objects.equals(email, oldUser.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, email, age);
    }

    @Override
    public String toString() {
        return "OldUser{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }
}

// ✅ Java 17: Record로 간결하게
public record User(String name, String email, int age) {
    // 생성자, getter, equals, hashCode, toString 자동 생성!
}

// 사용 예제
public class RecordBasicExample {
    public static void main(String[] args) {
        User user = new User("John Doe", "john@example.com", 30);

        // 자동 생성된 getter (필드명과 동일)
        System.out.println(user.name());  // "John Doe"
        System.out.println(user.email());  // "john@example.com"
        System.out.println(user.age());  // 30

        // 자동 생성된 toString
        System.out.println(user);  // User[name=John Doe, email=john@example.com, age=30]

        // 자동 생성된 equals
        User user2 = new User("John Doe", "john@example.com", 30);
        System.out.println(user.equals(user2));  // true
    }
}

Record의 고급 기능

// 커스텀 생성자 (Compact Constructor)
public record Person(String name, int age) {
    // Compact Constructor: 검증 로직 추가
    public Person {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be blank");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
        // 필드 초기화는 자동으로 수행됨
    }
}

// 정규 생성자
public record Employee(String name, String department, double salary) {
    public Employee(String name, String department) {
        this(name, department, 0.0);  // 기본 급여
    }
}

// 커스텀 메서드 추가
public record Point(int x, int y) {
    // 정적 팩토리 메서드
    public static Point origin() {
        return new Point(0, 0);
    }

    // 인스턴스 메서드
    public double distanceFromOrigin() {
        return Math.sqrt(x * x + y * y);
    }

    // 다른 점까지의 거리
    public double distanceTo(Point other) {
        int dx = this.x - other.x;
        int dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

// 실무 활용: DTO
public record UserDTO(
    Long id,
    String username,
    String email,
    LocalDateTime createdAt
) {
    // 엔티티에서 DTO로 변환
    public static UserDTO from(UserEntity entity) {
        return new UserDTO(
            entity.getId(),
            entity.getUsername(),
            entity.getEmail(),
            entity.getCreatedAt()
        );
    }
}

// 실무 활용: API 응답
public record ApiResponse<T>(
    boolean success,
    String message,
    T data,
    LocalDateTime timestamp
) {
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(true, "Success", data, LocalDateTime.now());
    }

    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>(false, message, null, LocalDateTime.now());
    }
}

// 실무 활용: 설정 객체
public record DatabaseConfig(
    String host,
    int port,
    String database,
    String username,
    String password
) {
    // 커스텀 생성자로 기본값 설정
    public DatabaseConfig {
        if (host == null) host = "localhost";
        if (port == 0) port = 3306;
    }

    public String getJdbcUrl() {
        return String.format("jdbc:mysql://%s:%d/%s", host, port, database);
    }
}

Record vs Lombok

// Lombok @Value
@Value
public class LombokUser {
    String name;
    String email;
    int age;
}

// Record (Java 17+)
public record RecordUser(String name, String email, int age) {}

// 차이점:
// 1. Record는 Java 표준 기능 (의존성 불필요)
// 2. Record는 final 클래스 (상속 불가)
// 3. Record는 모든 필드가 final
// 4. Lombok은 더 많은 기능 제공 (@Builder, @With 등)

// Record + Builder 패턴
public record Product(
    String id,
    String name,
    BigDecimal price,
    String category
) {
    // Builder 패턴 구현
    public static class Builder {
        private String id;
        private String name;
        private BigDecimal price;
        private String category;

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder price(BigDecimal price) {
            this.price = price;
            return this;
        }

        public Builder category(String category) {
            this.category = category;
            return this;
        }

        public Product build() {
            return new Product(id, name, price, category);
        }
    }

    public static Builder builder() {
        return new Builder();
    }
}

Sealed Classes: 상속 제한

Sealed Class의 필요성

// ❌ 문제: 모든 클래스가 상속 가능
public class PaymentMethod {
    // 누구나 상속 가능
}

// ✅ 해결책: Sealed Class
public sealed class PaymentMethod 
    permits CreditCard, DebitCard, Cash, PayPal {
    // 지정된 클래스만 상속 가능
}

// 허용된 하위 클래스들
public final class CreditCard extends PaymentMethod {
    private final String cardNumber;
    private final String cvv;

    public CreditCard(String cardNumber, String cvv) {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
    }

    // 메서드들...
}

public final class DebitCard extends PaymentMethod {
    private final String accountNumber;

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

public final class Cash extends PaymentMethod {
    private final BigDecimal amount;

    public Cash(BigDecimal amount) {
        this.amount = amount;
    }
}

public non-sealed class PayPal extends PaymentMethod {
    // non-sealed: 하위 클래스는 다시 확장 가능
    private final String email;

    public PayPal(String email) {
        this.email = email;
    }
}

Sealed Class와 Pattern Matching

public class PaymentProcessor {

    // Pattern Matching with Sealed Classes
    public BigDecimal processPayment(PaymentMethod payment, BigDecimal amount) {
        return switch (payment) {
            case CreditCard cc -> processCreditCard(cc, amount);
            case DebitCard dc -> processDebitCard(dc, amount);
            case Cash cash -> processCash(cash, amount);
            case PayPal pp -> processPayPal(pp, amount);
            // Sealed class이므로 default 불필요 (모든 케이스 처리)
        };
    }

    private BigDecimal processCreditCard(CreditCard card, BigDecimal amount) {
        System.out.println("Processing credit card: " + card);
        return amount.multiply(BigDecimal.valueOf(1.02));  // 2% 수수료
    }

    private BigDecimal processDebitCard(DebitCard card, BigDecimal amount) {
        System.out.println("Processing debit card: " + card);
        return amount.multiply(BigDecimal.valueOf(1.01));  // 1% 수수료
    }

    private BigDecimal processCash(Cash cash, BigDecimal amount) {
        System.out.println("Processing cash: " + cash);
        return amount;  // 수수료 없음
    }

    private BigDecimal processPayPal(PayPal paypal, BigDecimal amount) {
        System.out.println("Processing PayPal: " + paypal);
        return amount.multiply(BigDecimal.valueOf(1.03));  // 3% 수수료
    }
}

// 실무 활용: API 응답 타입
public sealed interface ApiResult<T> 
    permits Success, Error, Loading {
}

public record Success<T>(T data) implements ApiResult<T> {}
public record Error<T>(String message, Exception exception) implements ApiResult<T> {}
public record Loading<T>() implements ApiResult<T> {}

// 사용 예제
public class ApiResultHandler {
    public <T> void handleResult(ApiResult<T> result) {
        switch (result) {
            case Success<T> success -> 
                System.out.println("Success: " + success.data());
            case Error<T> error -> 
                System.err.println("Error: " + error.message());
            case Loading<T> loading -> 
                System.out.println("Loading...");
        }
    }
}

Pattern Matching for instanceof

instanceof의 진화

public class PatternMatchingExamples {

    // ❌ Java 16 이전: 타입 캐스팅 필요
    public void oldInstanceof(Object obj) {
        if (obj instanceof String) {
            String str = (String) obj;  // 명시적 캐스팅
            System.out.println(str.toUpperCase());
        }
    }

    // ✅ Java 17: Pattern Matching
    public void newInstanceof(Object obj) {
        if (obj instanceof String str) {
            // str은 자동으로 String 타입
            System.out.println(str.toUpperCase());
        }
    }

    // 복잡한 조건문
    public void complexConditions(Object obj) {
        if (obj instanceof String str && str.length() > 10) {
            System.out.println("Long string: " + str);
        } else if (obj instanceof Integer num && num > 100) {
            System.out.println("Large number: " + num);
        }
    }

    // 실무 활용: 다형성 처리
    public String formatValue(Object value) {
        if (value instanceof String str) {
            return "String: " + str;
        } else if (value instanceof Integer num) {
            return "Number: " + num;
        } else if (value instanceof LocalDate date) {
            return "Date: " + date.format(DateTimeFormatter.ISO_DATE);
        } else if (value instanceof List<?> list) {
            return "List of " + list.size() + " items";
        } else {
            return "Unknown: " + value;
        }
    }

    // equals 메서드에서 활용
    @Override
    public boolean equals(Object obj) {
        return obj instanceof PatternMatchingExamples other &&
               this.someField.equals(other.someField);
    }
}

Text Blocks: 멀티라인 문자열

Text Block의 편리함

public class TextBlockExamples {

    // ❌ Java 15 이전: 이스케이프 문자 필요
    public void oldMultilineString() {
        String json = "{\n" +
                      "  \"name\": \"John Doe\",\n" +
                      "  \"age\": 30,\n" +
                      "  \"email\": \"john@example.com\"\n" +
                      "}";

        String sql = "SELECT u.id, u.name, u.email\n" +
                     "FROM users u\n" +
                     "WHERE u.age > 18\n" +
                     "ORDER BY u.name";
    }

    // ✅ Java 17: Text Blocks
    public void newTextBlocks() {
        String json = """
            {
              "name": "John Doe",
              "age": 30,
              "email": "john@example.com"
            }
            """;

        String sql = """
            SELECT u.id, u.name, u.email
            FROM users u
            WHERE u.age > 18
            ORDER BY u.name
            """;
    }

    // 실무 활용: HTML 템플릿
    public String generateHtmlEmail(String userName, String content) {
        return """
            <!DOCTYPE html>
            <html>
            <head>
                <title>Email</title>
            </head>
            <body>
                <h1>Hello, %s!</h1>
                <p>%s</p>
                <footer>
                    <p>Best regards,<br>Your Team</p>
                </footer>
            </body>
            </html>
            """.formatted(userName, content);
    }

    // 실무 활용: JSON 템플릿
    public String createUserJson(String name, int age, String email) {
        return """
            {
              "user": {
                "name": "%s",
                "age": %d,
                "email": "%s",
                "createdAt": "%s"
              }
            }
            """.formatted(name, age, email, LocalDateTime.now());
    }

    // 실무 활용: SQL 쿼리
    public String complexQuery() {
        return """
            SELECT 
                u.id,
                u.name,
                u.email,
                COUNT(o.id) as order_count,
                SUM(o.amount) as total_amount
            FROM users u
            LEFT JOIN orders o ON u.id = o.user_id
            WHERE u.status = 'ACTIVE'
              AND u.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
            GROUP BY u.id, u.name, u.email
            HAVING order_count > 0
            ORDER BY total_amount DESC
            LIMIT 100
            """;
    }
}

8.3 Java 21 (최신 LTS)

Java 21의 혁신적인 기능들

Virtual Threads (Project Loom)

Virtual Threads의 혁명

public class VirtualThreadsExamples {

    // ❌ 전통적인 Platform Thread
    public void traditionalThreads() throws InterruptedException {
        long start = System.currentTimeMillis();

        // 10,000개의 스레드 생성 (메모리 부족 가능!)
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10_000; i++) {
            int taskId = i;
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("Task " + taskId + " completed");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            threads.add(thread);
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long duration = System.currentTimeMillis() - start;
        System.out.println("Duration: " + duration + "ms");
    }

    // ✅ Virtual Threads (Java 21+)
    public void virtualThreads() throws InterruptedException {
        long start = System.currentTimeMillis();

        // 100만개의 가상 스레드도 문제없음!
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 100_000; i++) {
                int taskId = i;
                executor.submit(() -> {
                    try {
                        Thread.sleep(1000);
                        System.out.println("Task " + taskId + " completed");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }  // executor.close() 자동 호출 (모든 태스크 완료 대기)

        long duration = System.currentTimeMillis() - start;
        System.out.println("Duration: " + duration + "ms");
    }

    // Virtual Thread 직접 생성
    public void createVirtualThread() {
        // 방법 1: Thread.ofVirtual()
        Thread vThread1 = Thread.ofVirtual().start(() -> {
            System.out.println("Virtual thread 1: " + Thread.currentThread());
        });

        // 방법 2: Thread.startVirtualThread()
        Thread vThread2 = Thread.startVirtualThread(() -> {
            System.out.println("Virtual thread 2: " + Thread.currentThread());
        });

        // 방법 3: Executors.newVirtualThreadPerTaskExecutor()
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                System.out.println("Virtual thread 3: " + Thread.currentThread());
            });
        }
    }

    // 실무 활용: HTTP 서버
    public void httpServerWithVirtualThreads() throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        server.createContext("/", exchange -> {
            // 각 요청마다 가상 스레드 생성
            Thread.startVirtualThread(() -> {
                try {
                    // I/O 작업 시뮬레이션
                    Thread.sleep(100);

                    String response = "Hello from Virtual Thread!";
                    exchange.sendResponseHeaders(200, response.length());

                    try (OutputStream os = exchange.getResponseBody()) {
                        os.write(response.getBytes());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        });

        server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        server.start();
        System.out.println("Server started on port 8080");
    }

    // 실무 활용: 병렬 데이터 처리
    public List<Result> processDataWithVirtualThreads(List<Data> dataList) 
            throws InterruptedException {

        List<Result> results = new CopyOnWriteArrayList<>();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (Data data : dataList) {
                executor.submit(() -> {
                    Result result = processData(data);
                    results.add(result);
                });
            }
        }  // 모든 작업 완료 대기

        return results;
    }

    private Result processData(Data data) {
        // 데이터 처리 로직
        return new Result();
    }
}

Virtual Threads vs Platform Threads 비교

public class ThreadComparison {

    // 성능 비교 테스트
    public static void main(String[] args) throws Exception {
        int taskCount = 10_000;
        int sleepTime = 1000;

        // Platform Threads
        long platformStart = System.currentTimeMillis();
        try (var executor = Executors.newFixedThreadPool(200)) {
            for (int i = 0; i < taskCount; i++) {
                executor.submit(() -> {
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }
        long platformDuration = System.currentTimeMillis() - platformStart;

        // Virtual Threads
        long virtualStart = System.currentTimeMillis();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < taskCount; i++) {
                executor.submit(() -> {
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }
        long virtualDuration = System.currentTimeMillis() - virtualStart;

        System.out.println("Platform Threads: " + platformDuration + "ms");
        System.out.println("Virtual Threads: " + virtualDuration + "ms");
        System.out.println("Speedup: " + 
            (double) platformDuration / virtualDuration + "x");
    }
}

Pattern Matching for switch

Switch 표현식의 진화

public class PatternMatchingSwitch {

    // ❌ Java 17 이전: instanceof 체인
    public String oldFormatValue(Object obj) {
        if (obj instanceof Integer i) {
            return String.format("int %d", i);
        } else if (obj instanceof Long l) {
            return String.format("long %d", l);
        } else if (obj instanceof Double d) {
            return String.format("double %f", d);
        } else if (obj instanceof String s) {
            return String.format("String %s", s);
        } else {
            return obj.toString();
        }
    }

    // ✅ Java 21: Pattern Matching for switch
    public String newFormatValue(Object obj) {
        return switch (obj) {
            case Integer i -> String.format("int %d", i);
            case Long l -> String.format("long %d", l);
            case Double d -> String.format("double %f", d);
            case String s -> String.format("String %s", s);
            case null -> "null";
            default -> obj.toString();
        };
    }

    // Guard 조건 (when)
    public String formatWithGuards(Object obj) {
        return switch (obj) {
            case String s when s.length() > 10 -> 
                "Long string: " + s.substring(0, 10) + "...";
            case String s -> 
                "Short string: " + s;
            case Integer i when i > 100 -> 
                "Large number: " + i;
            case Integer i -> 
                "Small number: " + i;
            case null -> 
                "null value";
            default -> 
                "Unknown: " + obj;
        };
    }

    // Record Pattern Matching
    public record Point(int x, int y) {}
    public record Circle(Point center, int radius) {}

    public String describeShape(Object shape) {
        return switch (shape) {
            case Circle(Point(int x, int y), int r) ->
                String.format("Circle at (%d, %d) with radius %d", x, y, r);
            case Point(int x, int y) ->
                String.format("Point at (%d, %d)", x, y);
            case null ->
                "null shape";
            default ->
                "Unknown shape";
        };
    }

    // 실무 활용: HTTP 상태 코드 처리
    public String handleHttpStatus(int statusCode) {
        return switch (statusCode) {
            case 200 -> "OK";
            case 201 -> "Created";
            case 204 -> "No Content";
            case 400 -> "Bad Request";
            case 401 -> "Unauthorized";
            case 403 -> "Forbidden";
            case 404 -> "Not Found";
            case 500 -> "Internal Server Error";
            case int code when code >= 200 && code < 300 -> 
                "Success: " + code;
            case int code when code >= 400 && code < 500 -> 
                "Client Error: " + code;
            case int code when code >= 500 -> 
                "Server Error: " + code;
            default -> 
                "Unknown status: " + statusCode;
        };
    }

    // 실무 활용: 도메인 객체 처리
    sealed interface PaymentStatus {}
    record Pending() implements PaymentStatus {}
    record Approved(String transactionId) implements PaymentStatus {}
    record Declined(String reason) implements PaymentStatus {}
    record Cancelled() implements PaymentStatus {}

    public String processPayment(PaymentStatus status) {
        return switch (status) {
            case Pending() -> 
                "Payment is pending...";
            case Approved(String txId) -> 
                "Payment approved with transaction: " + txId;
            case Declined(String reason) -> 
                "Payment declined: " + reason;
            case Cancelled() -> 
                "Payment was cancelled";
        };
    }
}

Sequenced Collections

순서가 있는 컬렉션의 표준화

public class SequencedCollectionsExamples {

    // SequencedCollection 인터페이스
    public void sequencedCollectionMethods() {
        // List는 SequencedCollection 구현
        List<String> list = new ArrayList<>(
            Arrays.asList("A", "B", "C", "D")
        );

        // 첫 번째와 마지막 요소 접근
        String first = list.getFirst();  // "A"
        String last = list.getLast();    // "D"

        // 첫 번째와 마지막 요소 추가
        list.addFirst("Z");  // ["Z", "A", "B", "C", "D"]
        list.addLast("E");   // ["Z", "A", "B", "C", "D", "E"]

        // 첫 번째와 마지막 요소 제거
        list.removeFirst();  // ["A", "B", "C", "D", "E"]
        list.removeLast();   // ["A", "B", "C", "D"]

        // 역순 뷰
        SequencedCollection<String> reversed = list.reversed();
        System.out.println(reversed);  // [D, C, B, A]
    }

    // SequencedSet
    public void sequencedSetMethods() {
        SequencedSet<Integer> set = new LinkedHashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);

        // 첫 번째와 마지막 요소
        Integer first = set.getFirst();  // 1
        Integer last = set.getLast();    // 3

        // 역순 뷰
        SequencedSet<Integer> reversed = set.reversed();
        System.out.println(reversed);  // [3, 2, 1]
    }

    // SequencedMap
    public void sequencedMapMethods() {
        SequencedMap<String, Integer> map = new LinkedHashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);

        // 첫 번째와 마지막 엔트리
        Map.Entry<String, Integer> firstEntry = map.firstEntry();  // A=1
        Map.Entry<String, Integer> lastEntry = map.lastEntry();    // C=3

        // 첫 번째와 마지막 키
        String firstKey = map.firstKey();  // "A"  (Java 21 이전에는 없었던 메서드)
        String lastKey = map.lastKey();    // "C"

        // 역순 뷰
        SequencedMap<String, Integer> reversed = map.reversed();
        System.out.println(reversed);  // {C=3, B=2, A=1}

        // 역순 키 셋
        SequencedSet<String> reversedKeys = map.sequencedKeySet().reversed();
        System.out.println(reversedKeys);  // [C, B, A]
    }

    // 실무 활용: LRU 캐시
    public class LRUCache<K, V> {
        private final int capacity;
        private final SequencedMap<K, V> cache;

        public LRUCache(int capacity) {
            this.capacity = capacity;
            this.cache = new LinkedHashMap<>();
        }

        public V get(K key) {
            V value = cache.remove(key);
            if (value != null) {
                cache.put(key, value);  // 최근 사용으로 이동
            }
            return value;
        }

        public void put(K key, V value) {
            cache.remove(key);
            cache.put(key, value);

            if (cache.size() > capacity) {
                // 가장 오래된 항목 제거
                cache.pollFirstEntry();
            }
        }

        public void clear() {
            cache.clear();
        }
    }
}

String Templates (Preview)

문자열 보간의 새로운 방식

public class StringTemplatesExamples {

    // ❌ 전통적인 문자열 포맷팅
    public void traditionalFormatting() {
        String name = "John";
        int age = 30;

        // String concatenation
        String msg1 = "Hello, " + name + "! You are " + age + " years old.";

        // String.format
        String msg2 = String.format("Hello, %s! You are %d years old.", name, age);

        // formatted (Java 15+)
        String msg3 = "Hello, %s! You are %d years old.".formatted(name, age);
    }

    // ✅ String Templates (Java 21 Preview)
    public void stringTemplates() {
        String name = "John";
        int age = 30;

        // String Template (STR processor)
        String msg = STR."Hello, \{name}! You are \{age} years old.";
        System.out.println(msg);

        // 표현식 사용
        String expression = STR."Next year, you'll be \{age + 1} years old.";

        // 메서드 호출
        String method = STR."Your name in uppercase: \{name.toUpperCase()}";
    }

    // 멀티라인 String Templates
    public void multilineTemplates() {
        String name = "John Doe";
        String email = "john@example.com";
        int age = 30;

        String profile = STR."""
            User Profile:
            -------------
            Name: \{name}
            Email: \{email}
            Age: \{age}
            Status: \{age >= 18 ? "Adult" : "Minor"}
            """;

        System.out.println(profile);
    }

    // FMT processor (포맷팅 포함)
    public void fmtProcessor() {
        double price = 1234.5678;
        int quantity = 3;

        String invoice = FMT."""
            Invoice
            -------
            Price: $%.2f\{price}
            Quantity: %d\{quantity}
            Total: $%.2f\{price * quantity}
            """;

        System.out.println(invoice);
    }

    // 실무 활용: SQL 쿼리 (안전하게)
    public void sqlQueryTemplate() {
        String tableName = "users";
        String columnName = "email";
        String value = "john@example.com";

        // 주의: 실제로는 PreparedStatement 사용 권장
        String query = STR."""
            SELECT *
            FROM \{tableName}
            WHERE \{columnName} = ?
            """;

        System.out.println(query);
    }
}

마치며

이번 글에서는 Java 8부터 최신 LTS 버전인 Java 21까지의 혁신적인 기능들을 실무 관점에서 깊이 있게 다뤄보았습니다.

핵심 요약:

Java 8-11:

  1. Lambda & Stream: 함수형 프로그래밍의 시작
  2. Optional: null 안전성 향상
  3. Date/Time API: 불변 날짜/시간 처리
  4. CompletableFuture: 비동기 프로그래밍
  5. 모듈 시스템: 대규모 애플리케이션 구조화
  6. var 키워드: 타입 추론으로 간결한 코드
  7. HTTP Client API: 현대적인 HTTP 통신

Java 17 LTS:

  1. Records: 불변 데이터 클래스의 간결한 표현
  2. Sealed Classes: 제한된 상속으로 타입 안전성 향상
  3. Pattern Matching for instanceof: 타입 체크와 캐스팅 간소화
  4. Text Blocks: 멀티라인 문자열의 가독성 향상

Java 21 LTS:

  1. Virtual Threads: 수백만 개의 경량 스레드로 동시성 혁명
  2. Pattern Matching for switch: 강력한 패턴 매칭
  3. Sequenced Collections: 순서가 있는 컬렉션의 표준화
  4. String Templates: 안전하고 편리한 문자열 보간

실무 적용 팁:

  1. 점진적 마이그레이션: 한 번에 모든 기능을 도입하지 말고, 프로젝트에 필요한 기능부터 단계적으로 적용
  2. 테스트 커버리지 확보: 새로운 기능 적용 전 충분한 테스트 작성
  3. 팀 교육: 새로운 문법과 패턴에 대한 팀원들의 이해도 확보
  4. 성능 측정: 특히 Virtual Threads 같은 경우 실제 워크로드에서 성능 측정 필수

다음 단계에서는:

  • 실전 프로젝트와 베스트 프랙티스
  • Clean Code와 테스트 주도 개발

를 다룰 예정입니다.

반응형