문법적 설탕 (Syntactic Sugar)
- 프로그래밍 언어에서 개발자에게 편리함을 제공하기 위해 기존의 기능을 더 간단하고 직관적으로 표현할 수 있도록 추가된 문법적 요소를 문법적 설탕(Syntactic Sugar)이라고 합니다.
- 쉽게 말해서 프로그래밍의 조미료 같은 역할이라 보시면 됩니다. (없으면 직접 만들어야하지만, 쓰면 너무 편리한..!)
 
 
- 이러한 문법적 설탕은 굳이 사용하지 않아도 기존의 방식으로도 충분히 구현할 수 있지만 이를 잘 활용하면 성능이나 가독성을 향상 시킬 수도 있습니다. 
- 기존 방식을 구현하는 것이 너무 불편해서 거의 필수적으로 쓰이는 것들도 상당히 많긴 합니다..!!
 
 
Java 버전 별 주요 문법적 설탕 정리
- Java에서는 다양한 버전 업데이트를 통해 새로운 기능과 문법적 설탕(Syntactic Sugar)을 도입해 개발자가 더 간결하고 직관적으로 코드를 작성할 수 있게 해왔습니다.
- 따라서 이러한 문법들은 사용하는 Java 버전이 어떤 버전인지를 고려해서 사용 여부를 결정해야합니다.
 
 
- 여기서는 버전 별로 문법적 설탕에 해당하는 주요 사항들을 정리해 보겠습니다! 
- 대체하기 힘든 새로운 기능들은 제외한 문법적 설탕들을 주로 정리했습니다.
 
 
Java 5 ~ 7
1. Enumerate (enum) - Java 5 도입
- 상수 그룹을 관리하는 더 효율적인 방법입니다. 
 
- 기존에는 
int나 String을 사용해 상수를 정의했지만, enum을 사용하면 타입 안정성을 보장할 수 있습니다. 
enum Day { MONDAY, TUESDAY, WEDNESDAY }
- 기존 방식대로 
final을 통해 정의해도 되지만, enum을 활용하면 훨씬 다양한 기능들을 추가하여 상수를 관리할 수 있습니다! 
enum Operation {
    PLUS {
        public int apply(int a, int b) {
            return a + b;
        }
    },
    MINUS {
        public int apply(int a, int b) {
            return a - b;
        }
    };
    public abstract int apply(int a, int b);
}
- 참고로 단순 상수 정의 외에도 메서드와 필드 등을 포함할 수 있으며, 복잡한 로직을 처리할 수 있습니다.
 
2. Enhanced for loop (for ( : )) - Java 5 도입
Iterable 인터페이스를 구현한 객체를 반복하는 문법으로, 기존 for문을 대체할 수 있습니다. 
- for문이나 while문으로도 충분히 동일한 기능을 구현할 수 있지만, 순회 가능한 요소들을 하나씩 접근해야할때 아주 유용하게 쓰입니다.
 
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
for (String item : list) {
    System.out.println(item);
}
- 다만, 추가적으로 NullPointerException 위험이 있어서 null이 포함된 객체에서는 예외가 발생할 수 있습니다.
 
3. Generics 및 Diamond Operator (<>) - Java 5 도입, Java 7에서 개선
- 제네릭을 사용하면 컬렉션 클래스의 타입을 지정하여 타입 안정성을 높일 수 있습니다. 
 
- Java 7에서는 다이아몬드 연산자(
<>)를 도입하여 타입을 추론하도록 개선되었습니다. 
List<String> list = new ArrayList<String>();
List<String> list = new ArrayList<>();
- 다이아몬드 연산자(
<>)가 문법적 설탕에 해당하고, 대부분의 IDE에서는 타입 추론이 가능할 때 시각적으로 이를 확인하여 사용하실 수 있습니다.
- 참고로 중첩된 제네릭 타입에서도 타입 추론이 가능합니다.
 
 
Map<String, List<Integer>> map = new HashMap<>();
4. Annotation (@) - Java 5 도입
- 주석처럼 보이는 특별한 구문으로 메타데이터를 코드에 포함시킬 수 있습니다. 
 
@Override, @Deprecated, @FunctionalInterface 등이 자주 사용됩니다. 
    @Override
    public void run() {
        System.out.println("Running");
    }
- 특히 
@Override가 가장 많이 쓰이고, 이를 명시하지 않아도 동일하게 동작하지만 명시하게되면 컴파일 시점에서 미구현시 발생 가능한 오류를 체크할 수 있게 됩니다. 
5. Try-With-Resource (try (resource) {}) - Java 7 도입
- 자원을 자동으로 닫아주는 문법입니다. 
 
- 기존 
try-catch-finally에서 자원 해제를 직접 관리할 필요 없이, try 블록에서 자동으로 자원을 닫을 수 있습니다.
- 다만 해당 클래스가 
AutoCloseable 인터페이스를 구현해야 사용 가능합니다. 
 
finally 키워드에서 열려있는 resource 들을 일일히 닫아줘도 되지만, Try-With-Resource를 이용하면 이를 생략할 수 있게 됩니다. 
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    return br.readLine();
}  
- 참고로 Java 9 에서는 좀 더 개선되어서 외부에서 선언한 객체를 resource로 변수의 형태로 사용 가능합니다.
 
final BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try (br) {
    return br.readLine();
}  
- 다만 이때는 
final이나 effectively final 변수만 사용할 수 있습니다. 
Java 8 (LTS)
1. Lambda Expression (()->{}) - Java 8 도입
- 함수형 인터페이스와 함께 사용하여 간결한 코드 작성을 가능하게 합니다. 
@FunctionalInterface Annotation과 함께 작성되어 단 한개의 추상메서드를 가지는 Interface를 함수형 인터페이스라 합니다. 
 
- 익명 클래스의 대체로 사용되며, 코드의 가독성을 높여줍니다.
 
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running");
    }
};
Runnable runnable = () -> System.out.println("Running");
(인수) -> {코드} 형태로 주로 쓰이고, 복잡해질 수 있는 코드를 1~2줄 정도로 축약시킬 수도 있습니다.
- 다만, 경우에 따라서는 오히려 가독성이 떨어지는 경우가 있어서 사용에는 주의하셔야합니다.
 
- 그래서 람다는 주로 짧고 간결한 동작에 적합합니다.
 
 
Consumer<String> printer = s -> System.out.println(s);
- 이후에 다룰 
Stream API 에서는 거의 필수적으로 많이 쓰이는 문법입니다.
- 다음 포스팅에서 람다 표현식과 함수형 인터페이스를 자세히 다룰 예정입니다.
 
 
2. Method Reference (::) - Java 8 도입
- 메서드 참조는 람다 표현식을 더 간단히 표현하는 방법입니다.
 
:: 형태로 주로 쓰입니다. 
Consumer<String> printer = (s) -> System.out.println(s);
Consumer<String> printer = System.out::println;
- 다음 포스팅에서 함수형 인터페이스와 함께 자세히 다룰 예정입니다.
 
3. Stream API (.stream()) - Java 8 도입
- 대용량 데이터 처리와 필터링, 매핑, 집계를 위한 강력한 도구입니다.
- 기존의 반복문이나 조건문을 대체하여 사용할 수 있습니다.
 
- 람다 표현식과 결합해 데이터를 직관적으로 처리할 수 있습니다.
 
 
- 지연 처리, 병렬 처리와 같은 기존에 없던 기능을 제공하기 때문에 문법적 설탕이라고 보긴 어렵긴 하지만, 병렬처리 상황이 아닌 경우엔 코드를 줄일 수 있는 문법적 설탕에 포함될 수 있다고 생각하여 내용에 포함시켰습니다. 
 
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println);
- 주의 하실 점은 지연처리되는 특성이 있어서 일반적인 반복문이나 조건문에 비해서 성능이 상당히 낮아질 수 있습니다.
- 그래서 병렬 처리가 필요 없으면서 속도에 대한 기준이 있는 코딩 테스트 등과 같은 상황에서는 사용하는걸 추천하진 않습니다.
 
 
- 다만 Stream의 병렬 처리를 사용할 경우 속도 향상이 가능한 경우도 있기 때문에 병렬 처리가 필요한 경우에 주로 쓰인다고 생각하시면 됩니다.
- Stream API는 기본적으로 순차적으로 데이터를 처리하지만, 
parallelStream()을 사용하면 병렬 처리가 가능합니다.  
- 그래서 대규모 데이터 처리 시 병렬 스트림을 활용하면 성능 향상이 기대될 수 있습니다.
 
 
Java 9 ~ 10
1. Collection.of() - Java 9 도입
- 불변 컬렉션을 더욱 쉽게 생성할 수 있습니다.
- 기존에는 
Collections의 unmodifiableXxx() 메서드로 불변 객체를 생성해야했고, 이 또한 읽기 전용 뷰를 반환하는 형태라서 원본 수정시 반영되는 문제가 있었습니다. 
 
List<String> list = List.of("one", "two", "three");
List.of(), Set.of(), Map.of() 등의 형태로 쓸 수 있습니다.  
- 참고로 
List.copyOf() 등의 불변 객체로 복사하는 메서드들도 동일하게 Java 9부터 도입되었습니다. 
2. var 키워드 - Java 10 도입
- 지역 변수에만 사용 가능하며, 컴파일러가 타입을 추론합니다.
- var는 지역 변수에만 사용 가능하며, 필드나 매개변수로는 사용할 수 없습니다.
 
 
- 코드가 더 간결해지며, 복잡한 타입 선언을 줄일 수 있습니다.
- 마치 동적 프로그래밍 언어처럼 쓸 수 있게 해줍니다만, 일각에서는 오히려 가독성이 떨어진다고 생각하기도 합니다.
 
- 참고로 JavaScript의 var 키워드와 비슷하다고 보시면 됩니다. (JS에서 var가 함수 스코프인 점에서도 어느정도 비슷하긴 합니다)
 
 
List<String> list = new ArrayList<>();
var list = new ArrayList<String>();
- 향상된 for문, 제네릭 타입, 등 다양한 경우에서 유용하게 쓰일 수 있습니다만, 선택의 영역이라서 타입을 명시적으로 지정해줘도 무방하긴합니다.
 
Java 11 (LTS) ~ 13
1. String 메서드 추가 - Java 11
- Java 11에서 문자열 관련 편의 메서드들이 추가되었습니다. 
 
- 공백 체크, 여러 줄 문자열 처리, 공백 제거 등이 가능해졌습니다.
.isBlank(), .lines(), .strip(), .repeat() 등 
 
String str = "  Hello World  ";
System.out.println(str.isBlank());  
System.out.println(str.strip());  
- 유사한 String 메서드와의 차이점
isBlank() : 길이가 0인지 여부만 따지는 .isEmpty()와는 다르게 공백만으로 구성되어 있어도 true를 반환합니다. 
.strip() : ASCII 공백 문자만 처리하는 .trim()과는 다르게 모든 종류의 공백문자(tab, 유니코드 공백 문자\u2000, 등)를 제거합니다. 
 
2. switch Expression 개선 - Java 12 ~ 14
- Java 12에서 switch를 표현식(
->)으로 사용 가능해졌습니다. (이때는 preview 기능이었습니다.)
- Java 13에서는 yeild 키워드를 통해 값을 반환할 수 있도록 개선되었습니다. (이 기능도 preview 기능이었습니다.)
yield는 값을 반환하고, 기존의 break는 흐름을 제어하는 역할을 합니다. 
 
- Java 14에서는 이러한 기능들이 정식적으로 표준화(Standard)되었습니다.
 
 
lambda 스타일의 문법을 지원하며, 각 코드에선 break문을 생략할 수 있습니다. 
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(1);
    case TUESDAY                -> System.out.println(2);
    case THURSDAY, SATURDAY     -> System.out.println(4);
    case WEDNESDAY              -> System.out.println(3);
}
var a = switch (day) {
    case MONDAY, FRIDAY, SUNDAY:
        yield 1;
    case TUESDAY:
        yield 2;
    case THURSDAY, SATURDAY:
        yield 4;
    case WEDNESDAY:
        yield 3;
};
- 이전 버전에서는 외부에서 변수를 선언하여 할당하는 방식으로 switch문을 사용해야하고, default를 제외한 각각의 case에 대해 break문을 사용해야 합니다.
 
3. Text Block (""") - Java 13 도입
- 여러 줄 문자열을 처리할 때 유용한 문법입니다.
 
- python의 
''', """과 동일하게 동작하고, 여러줄의 문자열을 개행문자(\n)없이 사용하여 가독성을 높일 수 있습니다. 
String json = """
    {
        "name": "Alice",
        "age": 25
    }
    """;
- 특히 JSON 파일 등을 관리하는 상황에서 상당히 유용하게 사용할 수 있습니다.
 
Java 14 ~ 17(LTS)
1. record 불변 객체 (record) - Java 14 도입
- 데이터를 저장하는 용도로 사용되는 불변 객체를 간결하게 정의할 수 있습니다.
- 변수의 타입과 이름을 이용해 
private final 필드를 자동 생성합니다. 
- 생성자, Getter, hashCode(), equals(), toString()도 자동 생성되고 오버라이딩 되어 기본적으로 제공하게 됩니다.
 
 
public record Person(String name, int age) {}
Person p = new Person("Alice", 25);
- 불변 객체로 데이터를 처리할 때 아주 유용합니다. 
- 특히 객체를 읽기 전용으로 안전하게 전달하고 싶을 때 
record를 사용하면 좋은 선택입니다. (DTO(Data Transfer Object), VO(Value Object), 등) 
 
2. Pattern Matching for instanceof - Java 14 도입
- instanceof와 동시에 타입 캐스팅을 할 수 있습니다.
 
- 타입 뒤에 바로 변수명을 지정하여 타입 체크와 변수 활용을 한번에 할 수 있게 됩니다.
 
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}
3. Pattern Matching for switch - Java 17 도입
- Java 17에서는 패턴 매칭을 활용하여 switch 문에서 더 복잡한 조건 처리가 가능해졌습니다.
 
Object obj = 1;
String result = switch (obj) {
    case Integer i -> "It's an integer";
    case String s -> "It's a string";
    default -> "Unknown type";
};
마무리
- 이번 포스팅에서는 Java의 버전별로 추가된 다양한 
문법적 설탕(Syntactic Sugar) 요소들을 살펴보았습니다. 
- 이러한 기능들은 개발자의 편의를 높이고, 코드의 가독성을 개선하기 위한 중요한 도구들입니다.
 
 
- 비록 이러한 문법적 요소 없이도 충분히 기존 방식으로 구현할 수 있지만, 문법적 설탕을 활용하면 개발 생산성과 코드의 간결함을 크게 향상시킬 수 있습니다. 
- 앞으로도 Java는 꾸준히 발전하며 새로운 기능들이 추가될 것이므로, 최신 버전에서 제공하는 문법적 설탕을 이해하고 사용하는 것이 중요합니다.
 
 
- 다음 포스팅에서는 람다 표현식과 함수형 인터페이스, 메서드 참조 등을 다뤄보겠습니다.