Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- Java
- stack
- 알고리즘
- redis
- MSA
- 백준
- 연습문제
- Gradle
- EDA
- code blocks
- 아키텍처
- Unity
- 탐색
- jre
- 트리
- 이진트리
- JPA
- Spring
- Kotlin
- bean
- jdk
- 프로그래머스
- Kafka
- 플러스 백엔드
- docker
- 오블완
- 삽입
- 코딩테스트
- event
- 티스토리챌린지
Archives
- Today
- Total
Repository
8단계: Java 기능 (Java 17+) 본문
반응형
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:
- Lambda & Stream: 함수형 프로그래밍의 시작
- Optional: null 안전성 향상
- Date/Time API: 불변 날짜/시간 처리
- CompletableFuture: 비동기 프로그래밍
- 모듈 시스템: 대규모 애플리케이션 구조화
- var 키워드: 타입 추론으로 간결한 코드
- HTTP Client API: 현대적인 HTTP 통신
Java 17 LTS:
- Records: 불변 데이터 클래스의 간결한 표현
- Sealed Classes: 제한된 상속으로 타입 안전성 향상
- Pattern Matching for instanceof: 타입 체크와 캐스팅 간소화
- Text Blocks: 멀티라인 문자열의 가독성 향상
Java 21 LTS:
- Virtual Threads: 수백만 개의 경량 스레드로 동시성 혁명
- Pattern Matching for switch: 강력한 패턴 매칭
- Sequenced Collections: 순서가 있는 컬렉션의 표준화
- String Templates: 안전하고 편리한 문자열 보간
실무 적용 팁:
- 점진적 마이그레이션: 한 번에 모든 기능을 도입하지 말고, 프로젝트에 필요한 기능부터 단계적으로 적용
- 테스트 커버리지 확보: 새로운 기능 적용 전 충분한 테스트 작성
- 팀 교육: 새로운 문법과 패턴에 대한 팀원들의 이해도 확보
- 성능 측정: 특히 Virtual Threads 같은 경우 실제 워크로드에서 성능 측정 필수
다음 단계에서는:
- 실전 프로젝트와 베스트 프랙티스
- Clean Code와 테스트 주도 개발
를 다룰 예정입니다.
반응형
'Java' 카테고리의 다른 글
| 9단계: 실전 프로젝트 & 베스트 프랙티스 (0) | 2025.12.11 |
|---|---|
| 7단계: 디자인 패턴 & 아키텍처 (0) | 2025.12.11 |
| 6단계: JVM & 성능 최적화 (0) | 2025.12.11 |
| 5단계: Java 동시성 & 멀티스레딩 (0) | 2025.12.11 |
| 4단계: Java 함수형 프로그래밍 (Java 8+) (0) | 2025.12.11 |