Java 17/21 LTS ๋ชจ๋ ๋ฌธ๋ฒ, SDKMAN ์ค์น, Record, Stream API, Optional, Virtual Threads, ๋์์ฑ, Sealed Class ๋ฐ JVM ๋ฉ๋ชจ๋ฆฌ ํ๋ ๋ ํผ๋ฐ์ค
## 1. JDK ์ค์น & ๋ฒ์ ๊ด๋ฆฌ (JDK Setup & SDKMAN!)
```bash
# โ [1] SDKMAN!์ ํตํ ๋ค์ค JDK ๋ฒ์ ๊ด๋ฆฌ (macOS / Linux / WSL2 ๊ฐ์ฅ ๊ถ์ฅ)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
# ์ง์๋๋ Java 17 / 21 LTS ๋ฐฐํฌํ ๋ชฉ๋ก ํ์ธ
sdk list java
# Eclipse Temurin(Adoptium) Java 21 ์ค์น ๋ฐ ๊ธฐ๋ณธ ๋ฒ์ ์ง์
sdk install java 21.0.3-tem
sdk default java 21.0.3-tem
# ํ๋ก์ ํธ๋ณ .sdkmanrc ์๋ ๋ฒ์ ์ ํ
sdk env init
sdk env
# ๐ช [2] Windows ํ๊ฒฝ ์ค์น (winget)
winget install EclipseAdoptium.Temurin.21.JDK
# JAVA_HOME ํ๊ฒฝ๋ณ์ ํ์ธ
echo %JAVA_HOME%
java -version
```
## 2. ๋ชจ๋ Java ํต์ฌ ๋ฌธ๋ฒ (Java 11 ~ 21 Features)
```java
// [1] ์ง์ญ ๋ณ์ ํ์
์ถ๋ก (var - Java 10+)
var message = "Hello, Java!"; // String ์๋ ์ถ๋ก
var userMap = new HashMap<Long, List<String>>(); // ๋ณต์กํ ์ ๋ค๋ฆญ ์ถ์ฝ
// [2] ํ
์คํธ ๋ธ๋ก (Text Blocks - Java 15+)
// ์ค๋ฐ๊ฟ๊ณผ ๋ฐ์ดํ๋ฅผ ์ด์ค์ผ์ดํ ์์ด ์ ์ง (JSON, SQL ์์ฑ ์ ํ์)
String json = """
{
"id": 1,
"name": "Lucky",
"role": "ADMIN"
}
""";
// [3] ํฅ์๋ Switch ํํ์ (Switch Expressions - Java 14+)
// ํ์ดํ(->) ๋ฌธ๋ฒ์ผ๋ก break ์๋ต ๊ฐ๋ฅ, ๊ฐ ๋ฐํ ์ง์
String result = switch (day) {
case MONDAY, FRIDAY -> "๋ฐ์ ๋ ";
case TUESDAY -> "ํ์";
case SATURDAY, SUNDAY -> {
System.out.println("์ฃผ๋ง ์๋ฆผ");
yield "ํด์"; // ๋ธ๋ก ๋ด์์๋ yield๋ก ๋ฐํ
}
default -> "ํ์ผ";
};
// [4] Pattern Matching for instanceof (Java 16+)
// ๋ถํ์ํ ๋ช
์์ ํ๋ณํ(Casting) ์ ๊ฑฐ
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase()); // ๋ฐ๋ก s ๋ณ์ ์ฌ์ฉ ๊ฐ๋ฅ
}
```
## 3. ๋ ์ฝ๋ & ๋ด์ธ๋ ํด๋์ค (Record & Sealed Class)
```java
// [1] Record (Java 16+ ๋ถ๋ณ ๋ฐ์ดํฐ ๊ฐ์ฒด DTO)
// ์์ฑ์, getter(id(), name()), equals(), hashCode(), toString() ์๋ ์์ฑ
public record UserDto(Long id, String name, String email) {
// ์ฝคํฉํธ ์์ฑ์ (์ ํจ์ฑ ๊ฒ์ฆ์ฉ)
public UserDto {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("์ด๋ฆ์ ํ์์
๋๋ค.");
}
}
// ์ปค์คํ
ํธ์ ๋ฉ์๋ ์ถ๊ฐ ๊ฐ๋ฅ
public String displayName() {
return name + " (" + email + ")";
}
}
// [2] Sealed Class (๋ด์ธ๋ ํด๋์ค - Java 17+)
// ์ง์ ํ ํ์ ํด๋์ค ์ธ์๋ ์์๋ก ์์/๊ตฌํ์ ์๊ฒฉํ ์ ํ
public sealed interface Shape permits Circle, Rectangle, Triangle { }
public final class Circle implements Shape { }
public final class Rectangle implements Shape { }
public non-sealed class Triangle implements Shape { } // ํ์ ํ์ฅ ํ์ฉ
```
## 4. ๋ชจ๋ ์ปฌ๋ ์
ํ๋ ์์ํฌ (Collection Factory Methods)
```java
// [1] ๋ถ๋ณ(Unmodifiable) ์ปฌ๋ ์
ํฉํ ๋ฆฌ (Java 9+)
List<String> list = List.of("A", "B", "C"); // ์์ ์ UnsupportedOperationException
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of(
"apple", 1000,
"banana", 2000
);
// 10๊ฐ ์ด๊ณผ ์ Map.ofEntries() ์ฌ์ฉ
Map<String, String> largeMap = Map.ofEntries(
Map.entry("k1", "v1"),
Map.entry("k2", "v2")
);
// [2] ๊ธฐ์กด ์ปฌ๋ ์
์ ์์ ํ ๋ถ๋ณ ๋ณต์ฌ๋ณธ (List.copyOf)
List<String> original = new ArrayList<>();
List<String> unmodifiableCopy = List.copyOf(original);
// [3] ์ ์ฉํ Map ํธ์ ๋ฉ์๋ (Java 8+)
Map<String, Integer> scores = new HashMap<>();
scores.putIfAbsent("Lucky", 100);
int current = scores.getOrDefault("Unknown", 0);
scores.computeIfPresent("Lucky", (k, v) -> v + 10); // 110์ผ๋ก ์
๋ฐ์ดํธ
scores.computeIfAbsent("NewUser", k -> 50); // ์์ผ๋ฉด 50 ํ ๋น
```
## 5. Stream API & ๋๋ค ํํ์ (Stream & Lambdas)
```java
List<User> users = List.of(
new User(1L, "Alice", 25, "SEOUL"),
new User(2L, "Bob", 35, "BUSAN"),
new User(3L, "Charlie", 28, "SEOUL")
);
// 1. ํํฐ๋ง, ๋งคํ ๋ฐ ์ ๋ ฌ
List<String> seoulUserNames = users.stream()
.filter(u -> "SEOUL".equals(u.city()))
.filter(u -> u.age() >= 20)
.sorted(Comparator.comparing(User::age).reversed()) // ๋์ด ๋ด๋ฆผ์ฐจ์
.map(User::name)
.toList(); // Java 16+ Unmodifiable List ๋ฐํ (Collectors.toList() ๋จ์ถ)
// 2. ๊ทธ๋ฃนํ (groupingBy - ๋์๋ณ ์ฌ์ฉ์ ๊ทธ๋ฃนํ)
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::city));
// 3. ํต๊ณ ๋ฐ ์ง๊ณ
double avgAge = users.stream()
.mapToInt(User::age)
.average()
.orElse(0.0);
// 4. flatMap (์ค์ฒฉ ๋ฆฌ์คํธ ํํํ)
List<List<String>> nested = List.of(List.of("A", "B"), List.of("C", "D"));
List<String> flat = nested.stream()
.flatMap(Collection::stream)
.toList(); // ["A", "B", "C", "D"]
```
## 6. Optional ์์ ํ ๋ ์ฒ๋ฆฌ (Null-Safe Optional)
```java
// [1] Optional ์์ฑ
Optional<String> opt = Optional.of("Hello"); // null ๋ถ๊ฐ (NPE ๋ฐ์)
Optional<String> optNull = Optional.ofNullable(null); // null ํ์ฉ -> Optional.empty()
// [2] ๊ฐ ์ถ์ถ ๋ฐ ๋์ฒด (์ํฐํจํด: get()์ด๋ isPresent() ๋จ์ฉ ๊ธ์ง!)
// ๊ถ์ฅ ํจํด A: orElse (๊ธฐ๋ณธ๊ฐ์ด ์ด๋ฏธ ์์ฑ๋ ์์์ผ ๋)
String val1 = optNull.orElse("DEFAULT");
// ๊ถ์ฅ ํจํด B: orElseGet (๊ธฐ๋ณธ๊ฐ ์์ฑ์ ๋น์ฉ์ด ๋๋ ์ฐ์ฐ์ผ ๋ Lazy ํ๊ฐ)
String val2 = optNull.orElseGet(() -> computeDefaultValue());
// ๊ถ์ฅ ํจํด C: orElseThrow (๊ฐ์ด ์์ผ๋ฉด ์ปค์คํ
์์ธ ๋์ง๊ธฐ)
User user = findUserById(id)
.orElseThrow(() -> new UserNotFoundException("์ฌ์ฉ์ ์์: " + id));
// [3] ๊ฐ ๋ณํ ์ฒด์ด๋
String upper = Optional.ofNullable(user)
.map(User::getEmail)
.filter(email -> email.contains("@"))
.map(String::toUpperCase)
.orElse("NO_EMAIL");
```
## 7. ๊ฐ์ ์ค๋ ๋ & ๋์์ฑ (Virtual Threads - Java 21)
```java
// [1] Virtual Threads (๊ฒฝ๋ ๊ฐ์ ์ค๋ ๋ - ์์ญ๋ง ๊ฐ ์์ฑ ๊ฐ๋ฅ)
// I/O ๋ธ๋กํน(DB, HTTP ํธ์ถ) ์ OS ์ค๋ ๋๋ฅผ ์ ์ ํ์ง ์๊ณ ์ธ๋ง์ดํธ๋จ
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // I/O ๋๊ธฐ ํ๋ด
return "Task " + i;
});
});
} // ๋ชจ๋ ์์
์ด ๋๋ ๋๊น์ง ์๋ ๋๊ธฐ (AutoCloseable)
// [2] CompletableFuture ๋น๋๊ธฐ ํ์ดํ๋ผ์ธ
CompletableFuture.supplyAsync(() -> fetchExternalApi())
.thenApply(json -> parseData(json))
.thenAccept(data -> saveToDatabase(data))
.exceptionally(ex -> {
log.error("๋น๋๊ธฐ ์ฒ๋ฆฌ ์๋ฌ", ex);
return null;
});
// [3] ๊ฐ์ ์ค๋ ๋ ์์ฑ ์ง์ ์คํ
Thread vThread = Thread.ofVirtual()
.name("virtual-worker-", 1)
.start(() -> doJob());
```
## 8. ์ ๋ค๋ฆญ & ์์ผ๋์นด๋ (Generics & PECS)
```java
// PECS ์์น: Producer Extends, Consumer Super
// ๋ฐ์ดํฐ๋ฅผ ๊บผ๋ด์ค๊ธฐ๋ง ํ๋ฉด (Producer) -> <? extends T>
// ๋ฐ์ดํฐ๋ฅผ ์ง์ด๋ฃ๊ธฐ๋ง ํ๋ฉด (Consumer) -> <? super T>
// 1. ๊ณต๋ณ์ฑ (Covariance: ๊บผ๋ด๊ธฐ ์ฝ๊ธฐ ์ ์ฉ)
public void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n.doubleValue());
}
// list.add(10); // ์ปดํ์ผ ์๋ฌ! (๋ฌด์์ด ๋ค์ด์ฌ์ง ๋ชฐ๋ผ ์ถ๊ฐ ๋ถ๊ฐ)
}
// 2. ๋ฐ๊ณต๋ณ์ฑ (Contravariance: ๋ฃ๊ธฐ ์ฐ๊ธฐ ์ ์ฉ)
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2); // Integer ๋ฐ ํ์ ๊ฐ์ฒด ์ถ๊ฐ ์์
// Object obj = list.get(0); // ๊บผ๋ผ ๋๋ Object๋ก๋ง ์์
}
// 3. ์ ๋ค๋ฆญ ๋ฉ์๋ ์ ์
public <T> T getFirst(List<T> items) {
return items.isEmpty() ? null : items.get(0);
}
```
## 9. ์์ธ ์ฒ๋ฆฌ & ๋ฆฌ์์ค ๊ด๋ฆฌ (Try-with-Resources)
```java
// [1] Try-with-Resources (Java 7+ / 9+ ํฅ์)
// AutoCloseable ๊ตฌํ ๊ฐ์ฒด๋ ๋ธ๋ก ์ข
๋ฃ ์ finally ์์ด ์๋ close() ๋ณด์ฅ
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"));
Connection conn = dataSource.getConnection()) {
String line = br.readLine();
// ๋น์ฆ๋์ค ์ฟผ๋ฆฌ ์คํ
} catch (IOException | SQLException e) { // ๋ค์ค ์์ธ ์ฒ๋ฆฌ
log.error("I/O ๋๋ DB ์์ธ ๋ฐ์: ", e);
throw new CustomBusinessException("์์
์คํจ", e);
}
// [2] ์ฌ์ฉ์ ์ ์ ๋น์ฆ๋์ค ์์ธ (Unchecked ์์ธ ์์ ๊ถ์ฅ)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
```
## 10. ํ์ผ I/O & ํ๋์ NIO.2 (Files & Path)
```java
import java.nio.file.*;
Path path = Path.of("logs", "app.log"); // Java 11+ Path.of()
// 1. ํ์ผ ์ ์ฒด๋ฅผ ๋ฌธ์์ด๋ก ์ฝ๊ธฐ & ์ฐ๊ธฐ (Java 11+)
String content = Files.readString(path, StandardCharsets.UTF_8);
Files.writeString(Path.of("output.txt"), "์ ๋ด์ฉ", StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// 2. ๋์ฉ๋ ํ์ผ ํ ์ค์ฉ ์คํธ๋ฆผ์ผ๋ก ์ฒ๋ฆฌ (๋ฉ๋ชจ๋ฆฌ ํจ์จ์ )
try (var lines = Files.lines(path)) {
lines.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// 3. ๋๋ ํ ๋ฆฌ ๊ฒ์ฌ ๋ฐ ์์ฑ
if (!Files.exists(path)) {
Files.createDirectories(path.getParent());
}
```
## 11. ์ธํฐํ์ด์ค & ๊ฐ์ฒด์งํฅ ์ค๊ณ (Interface Evolution)
```java
public interface PaymentGateway {
// 1. ์ถ์ ๋ฉ์๋ (๋ฐ๋์ ๊ตฌํ ํ์)
PaymentResult pay(int amount);
// 2. ๋ํดํธ ๋ฉ์๋ (default - ํ์ ํธํ์ฑ ๋ณด์ฅ ๋ฐ ๊ณตํต ๋ก์ง ์ ๊ณต)
default void sendReceipt(String email, int amount) {
System.out.println("์์์ฆ ๋ฐ์ก to " + email + ": " + amount + "์");
}
// 3. ์ ์ ์ ํธ๋ฆฌํฐ ๋ฉ์๋ (static)
static boolean validateAmount(int amount) {
return amount > 0;
}
// 4. ๋น๊ณต๊ฐ ๋ฉ์๋ (private - Java 9+ ๋ํดํธ ๋ฉ์๋ ๋ด๋ถ ์ค๋ณต ๋ก์ง ์บก์ํ)
private void logTransaction(String type) {
System.out.println("[LOG] ํธ๋์ญ์
: " + type);
}
}
```
## 12. ์ค๋ฌด JVM ํ๋ & ๋ฉ๋ชจ๋ฆฌ ๊ตฌ์กฐ (JVM Performance & Flags)
```text
# [1] JVM ๋ฉ๋ชจ๋ฆฌ ํต์ฌ ์์ญ
1. Heap Area:
- ๋ชจ๋ ๊ฐ์ฒด(์ธ์คํด์ค)๊ฐ ํ ๋น๋๋ ์์ญ
- Young Generation (Eden, S0, S1) + Old Generation
2. Metaspace (Java 8+):
- ํด๋์ค ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ (๋ค์ดํฐ๋ธ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ, PermGen ๋์ฒด)
3. JVM Stack:
- ๊ฐ ์ค๋ ๋๋ณ ๋ฉ์๋ ํธ์ถ ํ๋ ์, ์ง์ญ ๋ณ์ ์ ์ฅ
# [2] ์ค๋ฌด ํ์ JVM ์์ ์ต์
(์ปจํ
์ด๋/๋์ปค ํ๊ฒฝ)
java -server \
-XX:+UseG1GC \ # G1GC ๊ธฐ๋ณธ ๊ฐ๋น์ง ์ปฌ๋ ํฐ (์ง์ฐ์๊ฐ ์ต์ํ)
-XX:MaxRAMPercentage=75.0 \ # ์ปจํ
์ด๋ ๋ฉ๋ชจ๋ฆฌ์ 75%๋ฅผ ํ์ผ๋ก ์๋ ํ ๋น
-XX:+UseContainerSupport \ # ๋์ปค cgroup ๋ฉ๋ชจ๋ฆฌ ์ ํ ์ธ์
-XX:+HeapDumpOnOutOfMemoryError \ # OOM ๋ฐ์ ์ ํ ๋คํ ์๋ ์์ฑ
-XX:HeapDumpPath=/var/log/heapdump.hprof \
-Dfile.encoding=UTF-8 \ # UTF-8 ํ์ผ ์ธ์ฝ๋ฉ ๊ฐ์
-jar app.jar
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์