개요

Spread 연산자(...)는 Dart 2.3 버전에서 도입된 기능으로, 컬렉션의 요소들을 다른 컬렉션에 편리하게 삽입할 수 있게 해주는 연산자입니다. 이번 포스트에서는 Spread 연산자의 다양한 사용법과 활용 사례에 대해 알아보겠습니다.

 

 

 

Spread 연산자

 

List에서의 사용

void main() {
  var list1 = [1, 2, 3];
  var list2 = [4, 5, 6];
  
  // Spread 연산자를 사용하여 리스트 병합
  var combined = [...list1, ...list2];
  print(combined);  // [1, 2, 3, 4, 5, 6]
  
  // 개별 요소와 함께 사용
  var withMore = [0, ...list1, 4];
  print(withMore);  // [0, 1, 2, 3, 4]
}

 

Set에서의 사용

void main() {
  var set1 = {1, 2, 3};
  var set2 = {3, 4, 5};
  
  // Set 병합 (중복제거)
  var combinedSet = {...set1, ...set2};
  print(combinedSet);  // {1, 2, 3, 4, 5}
}

 

 

Map에서의 사용

void main() {
  var map1 = {'a': 1, 'b': 2};
  var map2 = {'c': 3, 'd': 4};
  
  // Map 병합
  var combinedMap = {...map1, ...map2};
  print(combinedMap);  // {a: 1, b: 2, c: 3, d: 4}
  
  // 충돌이 있는 경우 나중에 오는 값이 우선
  var map3 = {'a': 5, 'e': 6};
  var overrideMap = {...map1, ...map3};
  print(overrideMap);  // {a: 5, b: 2, e: 6}
}

 

 

Null-aware Spread 연산자 (...?)

void main() {
  List<int>? nullableList;
  var list = [1, 2, ...?nullableList];
  print(list);  // [1, 2]
  
  nullableList = [3, 4];
  list = [1, 2, ...?nullableList];
  print(list);  // [1, 2, 3, 4]
}

 

조건부 사용

void main() {
  var items = [1, 2];
  List<int>? extraItems;
  
  // null이 아닐 때만 spread
  var combined = [
    ...items,
    if (extraItems != null) ...extraItems,
  ];
  print(combined);  // [1, 2]
}

 

 

 

Spread 활용

 

Flutter 위젯 트리에서의 활용

class MyWidget extends StatelessWidget {
  final List<Widget>? optionalWidgets;
  
  const MyWidget({Key? key, this.optionalWidgets}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Fixed Widget'),
        ...?optionalWidgets,
        Text('Another Fixed Widget'),
      ],
    );
  }
}

 

데이터 모델 복사 및 업데이트

class User {
  final String name;
  final Map<String, dynamic> preferences;
  
  User(this.name, this.preferences);
  
  User copyWith({
    String? name,
    Map<String, dynamic>? extraPreferences,
  }) {
    return User(
      name ?? this.name,
      {
        ...preferences,
        if (extraPreferences != null) ...extraPreferences,
      },
    );
  }
}

 

API 응답 병합

Future<Map<String, dynamic>> fetchUserData() async {
  final basicInfo = await fetchBasicInfo();
  final preferences = await fetchPreferences();
  final settings = await fetchSettings();
  
  return {
    ...basicInfo,
    'preferences': {...preferences},
    'settings': {...settings},
  };
}

 

 

더 자세히

중첩된 컬렉션

void main() {
  var nested = [
    [1, 2],
    [3, 4],
    [5, 6],
  ];
  
  // 중첩 리스트 펼치기
  var flattened = [
    for (var list in nested) ...list,
  ];
  print(flattened);  // [1, 2, 3, 4, 5, 6]
}

 

메모리 사용

// 새로운 리스트 생성 - 메모리 사용
var newList = [...oldList];  // 전체 복사

// 참조만 생성 - 메모리 효율적
var reference = oldList;     // 참조만 복사

 

 

대규모 컬렉션 처리

void processLargeCollections() {
  var largeList1 = List.generate(10000, (i) => i);
  var largeList2 = List.generate(10000, (i) => i + 10000);
  
  // 메모리 사용량이 큼
  var combined = [...largeList1, ...largeList2];
  
  // 대안: Iterator 사용
  var iterator = Chain([largeList1.iterator, largeList2.iterator]);
}

 

 

주의사항

 

타입 안전성

void main() {
  List<int> numbers = [1, 2, 3];
  List<String> strings = ['a', 'b', 'c'];
  
  // 컴파일 에러: 타입 불일치
  // List<int> combined = [...numbers, ...strings];
  
  // 올바른 방법: dynamic 또는 Object 사용
  List<dynamic> combined = [...numbers, ...strings];
}

 

 

Const 컨텍스트

// const 컨텍스트에서 사용 가능
const list1 = [1, 2, 3];
const list2 = [4, 5, 6];
const combined = [...list1, ...list2];  // OK

// 런타임 값에는 const 사용 불가
var runtime = [1, 2, 3];
const invalid = [...runtime];  // Error

 

 

 

결론

Spread 연산자는 컬렉션을 다룰 때 매우 유용한 도구입니다. 특히 Flutter 개발에서 위젯 트리를 구성할 때 자주 사용되며, 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 다만, 대규모 컬렉션을 다룰 때는 메모리 사용량을 고려해야 하며, 타입 안전성에도 주의를 기울여야 합니다.

'Language > Flutter' 카테고리의 다른 글

[Dart] fold() 메소드  (0) 2024.11.19
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15
[Flutter] Widget의 크기제약  (0) 2024.11.07

개요

Dart 2.12 버전부터 도입된 Null Safety는 널 참조로 인한 런타임 에러를 방지하기 위한 기능입니다. 이를 통해 개발자는 컴파일 타임에 잠재적인 널 참조 오류를 발견하고 수정할 수 있습니다. 바로 들어가봅시다.
 
 

Non-nullable 타입

기본적으로 모든 타입은 null을 허용하지 않습니다.

String name = 'John';     // OK
int age = 25;            // OK
String name = null;      // Error
int age = null;         // Error

 

Nullable 타입

타입 뒤에 ?를 붙여 null을 허용할 수 있습니다.

String? nullableName = 'John';  // OK
nullableName = null;           // OK

int? nullableAge = 25;         // OK
nullableAge = null;           // OK

 
 

Null 검사 연산자

null 인식 연산자 (?)

객체가 null일 수 있는 경우 안전하게 접근하기 위해 사용합니다.

String? name = 'John';
print(name?.length);    // name이 null이 아니면 길이 출력, null이면 null 출력

String? nullName = null;
print(nullName?.length);  // null 출력

// 체이닝
class User {
  String? email;
}

User? user = User();
print(user?.email?.length);  // email이 null이면 null 출력

 
 

null 주장 연산자 (!)

nullable 타입을 non-nullable 타입으로 강제 변환합니다. null일 경우 런타임 에러가 발생하므로 주의해서 사용해야 합니다.

String? nullableName = 'John';
String nonNullableName = nullableName!;  // OK

String? nullName = null;
String nonNullName = nullName!;  // Runtime Error

 
 

null 병합 연산자 (??)

왼쪽 피연산자가 null인 경우 오른쪽 피연산자를 반환합니다.

String? name = null;
String displayName = name ?? 'Anonymous';  // 'Anonymous' 출력

String? firstName = null;
String? middleName = null;
String? lastName = 'Doe';
String displayName = firstName ?? middleName ?? lastName ?? 'Anonymous';  // 'Doe' 출력

 
 

late 키워드와 Null Safety

late 변수 선언

초기화를 나중에 하는 non-nullable 변수를 선언할 수 있습니다.

class User {
  late String email;  // 나중에 초기화할 non-nullable 변수
  
  void init(String userEmail) {
    email = userEmail;  // 초기화
  }
}

 
 

late final 변수

한 번만 초기화할 수 있는 변수를 선언합니다.

class User {
  late final String email;
  
  void init(String userEmail) {
    email = userEmail;  // 첫 번째 초기화 OK
    email = 'new@email.com';  // Error: final 변수는 재할당 불가
  }
}

 
 

주의사항

null 주장 연산자(!) 사용 시 주의사항

// 위험한 코드
void processUser(User? user) {
  print(user!.name);  // user가 null이면 런타임 에러
}

// 안전한 코드
void processUser(User? user) {
  if (user == null) return;
  print(user.name);  // null 체크 후 안전하게 접근
}

 
조건부 속성 접근

class User {
  final String? email;
  
  User({this.email});
  
  bool get hasEmail => email?.isNotEmpty == true;
  
  String get displayEmail => email ?? 'No email provided';
}

 
컬렉션에서의 Null Safety

// 리스트 요소가 null일 수 있는 경우
List<String?> nullableStrings = ['Hello', null, 'World'];

// 리스트 자체가 null일 수 있는 경우
List<String>? nullableList = null;

// 둘 다 null일 수 있는 경우
List<String?>? bothNullable = null;

// null이 아닌 요소만 처리
void processStrings(List<String?> strings) {
  for (final string in strings) {
    if (string != null) {
      print(string.toUpperCase());
    }
  }
}

 
 

결론

Null Safety는 코드의 안정성을 크게 향상시키는 중요한 기능입니다. 적절히 사용하면 널 참조 오류를 미리 방지하고, 코드의 의도를 더 명확하게 표현할 수 있습니다. 특히 Flutter 애플리케이션 개발에서는 필수적인 기능이므로, 잘 이해하고 활용하시기 바랍니다.

개요

Dart에서는 변수를 선언할 때 다양한 키워드를 사용할 수 있습니다. 각 키워드는 서로 다른 특성과 용도를 가지고 있어, 상황에 맞게 적절한 키워드를 선택하는 것이 중요합니다. 이번 글에서는 각 키워드의 특징과 사용법에 대해 자세히 알아보겠습니다.
 
 
 

var

var는 타입 추론을 사용하는 변수 선언 방식입니다. 변수를 초기화할 때 할당되는 값의 타입에 따라 자동으로 변수의 타입이 결정됩니다.
 

void main() {
  var name = 'John';     // String으로 추론
  var age = 25;          // int로 추론
  var height = 175.5;    // double로 추론
  var isStudent = true;  // bool로 추론
  var numbers = [1,2,3]; // List<int>로 추론
}

 
 

특징

한번 추론된 타입은 변경할 수 없습니다.

void main() {
    var name = 'Jone';
    name = 'Jane';
    
    name = 10; // ERROR !!
}

 
 
초기값을 지정하지않으면 dynamic 타입이 됩니다.

void main() {
    var name;
    name = 'Jane';
    
    name = 10; // OK
}

 
 
 

dynamic

dynamic은 모든 타입의 값을 저장할 수 있는 특별한 타입입니다. 런타임에 타입 체크가 이루어집니다.
 

void main() {
  dynamic value = 'Hello';
  print(value.runtimeType);  // String
  
  value = 42;
  print(value.runtimeType);  // int
  
  value = true;
  print(value.runtimeType);  // bool
}

 
 

특징

타입에 제약을 받지않고 할당할 수 있습니다.

dynamic value = 'Hello';
value = 42;        // OK
value = true;      // OK
value = [1,2,3];   // OK

 
다만, 런타임 에러의 위험이 있습니다.

dynamic value = 42;
print(value.length);  // Runtime Error

 
주로 JSON 파싱과 같이 타입이 동적으로 결정되는 경우 사용됩니다.
 
 
 

final

final은 한 번만 값을 할당할 수 있는 불변 변수를 선언할 때 사용합니다.

void main() {
  final String name = 'John';
  final age = 25;  // 타입 추론 가능
  
  name = 'Jane';  // Error
}

 
 

특징

런타임에 값이 결정될 수 있습니다.

final time = DateTime.now();  // OK
final random = Random().nextInt(100);  // OK

 
클래스의 인스턴스 변수로 사용 가능합니다.

class Person {
  final String name;
  final int age;
  
  Person(this.name, this.age);
}

 
 
 

late

late는 변수를 나중에 초기화할 것임을 명시하는 키워드입니다.

void main() {
  late String name;
  
  // 나중에 초기화
  name = 'John';
  print(name);  // OK
}

 
 

특징

null safety와 함께 사용됩니다.

class Person {
  late String name;  // null이 아닌 값으로 반드시 초기화되어야 함
  
  void init(String userName) {
    name = userName;
  }
}

 
초기화하기전에 에러가 발생합니다.

late String name;
print(name);  // Error: LateInitializationError

 
lazy initialization 이 가능합니다.

late String expensiveOperation = _loadData();
// expensiveOperation이 실제로 사용될 때만 _loadData() 호출

 
 
 

const

const는 컴파일 타임 상수를 선언할 때 사용합니다.

void main() {
  const PI = 3.14159;
  const int maxAttempts = 3;
  const String greeting = 'Hello';
}

 
 

특징

컴파일 타임에 값이 결정되어야합니다.

const time = DateTime.now();  // Error: 컴파일 타임에 값을 알 수 없음
const random = Random().nextInt(100);  // Error

 
깊은 불변성을 가집니다.

const list = [1, 2, 3];  // 리스트의 내용도 변경 불가
const map = {'name': 'John', 'age': 25};  // 맵의 내용도 변경 불가

 
메모리 효율성이 있습니다.

const v1 = [1, 2, 3];
const v2 = [1, 2, 3];
print(identical(v1, v2));  // true: 같은 메모리 공간 공유

 
 
 
 

성능과 메모리 고려사항

const vs final

  • const는 컴파일 타임 상수로, 메모리를 더 효율적으로 사용
  • final은 런타임에 값이 결정되어 약간의 오버헤드 발생

var vs dynamic

  • var는 타입이 고정되어 있어 성능상 이점
  • dynamic은 런타임 타입 체크로 인한 오버헤드 발생

late 키워드의 영향

  • 초기화 체크로 인한 약간의 런타임 오버헤드
  • 메모리는 실제 초기화 시점에만 할당

 

결론

각 키워드는 그만의 특징과 용도가 있습니다. 상황에 맞는 적절한 키워드를 선택하는 것이 중요합니다:

  • var: 지역 변수, 타입이 명확한 경우
  • dynamic: 타입이 동적으로 변하는 경우
  • final: 한 번 초기화 후 변경되지 않는 값
  • late: 지연 초기화가 필요한 경우
  • const: 컴파일 타임 상수

코드의 가독성, 유지보수성, 그리고 성능을 고려하여 적절한 키워드를 선택하시기 바랍니다.

StringTokenizer

StringTokenizer는 문자열을 지정된 구분자를 기준으로 토큰화하는 Java의 유틸리티 클래스입니다.

 

        String str = "a;b;c;d;e";

        StringTokenizer st = new StringTokenizer(str, ";");
        while (st.hasMoreTokens()) {
            System.out.print(st.nextToken()); // abcde
        }
        
        StringTokenizer st = new StringTokenizer(str, "abc");
        while (st.hasMoreTokens()) {
            System.out.print(st.nextToken()); // de
        }

가령 "a;b;c;d;e" 라는 문자열에서 ";" 문자열로 나누어 토큰화 하고싶다면 위 처럼 사용하는 것입니다.

위와 같은 역할을 수행하는게 split() 메소드인데요 split() 메소는 정규식을 사용하기때문에 활용성이 높고 정규식을 잘 이해하고있다면 더욱 편한게 사실입니다.

그러면 이 StringTokenizer를 왜 사용했을까요? split() 메소드는 내부에서 정규식을 사용하기때문에 StringTokenizer보다 속도가 4배정도 느리다고 합니다. 그래서 속도가 중요한 부분에서 이 StringTokenizer를 사용했었죠. 하지만 현재는 사용하지않는것을 권장하고있습니다. 이 글에서 이 부분에 대해서 알아보겠습니다.

 

 

 

 

Document

JAVA 5 StringTokenizer

JAVA 6 StringTokenizer

JAVA 7 StringTokenizer

JAVA 8 StringTokenizer

JAVA 9 StringTokenizer

JAVA 10 StringTokenizer

JAVA 11 StringTokenizer

JAVA 21 StringTokenizer

 

StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead.

StringTokenizer는 호환성을 이유로 유지되는 레거시 클래스이지만 새 코드에서는 사용이 권장되지 않습니다. 이 기능을 원하는 사람은 대신 String의 split 메서드나 java.util.regex 패키지를 사용하는 것이 좋습니다.

 

문서에는 StringTokenizer는 새 코드에서는 사용하는것을 권장하지 않는다고 작성되어있습니다.

 

StringTokenizer의 주석에도 권장하지 않는다고 하는데 왜 그런지 간단하게 알아보겠습니다.

 

 

 

 

권장하지 않는 이유

 

유연성의 문제

StringTokenizer는 매우 단순한 방식으로 문자열을 분리합니다. 기본적으로 정규 표현식을 지원하지 않기 때문에 복잡한 문자열 처리에는 한계가 있습니다.  split() 메소드나 java.util.regex 패키지를 사용하면 정규표현식을 지원하여 복잡한 구분자를 처리하여 더 유연하고 안정적인 방식으로 문자열을 분리할 수 있습니다. 그러니 StringTokenizer를 사용할 이유가 없어지고 있는 것이지요.

 

 

클래스 개선문제

StringTokenizer는 Java 초기에 도입된 클래스입니다. 이후 자바가 발전하면서 더 나은 기능을 제공하는 클래스들이 도입되면서 StringTokenizer의 사용이유가 더욱 줄어들었습니다. 따라서 새로운 기능이 추가되거나 개선되지 않습니다.

 

제한된 기능

정규표현식을 지원하지않고, 복합한 문자열 처리에 한계가있습니다. 또한 반환 타입이 String[] 같은 배열이 아닙니다.

 

 

 

 

결론

StringTokenizer의 장점도 존재합니다. 정규표현식이 없기때문에 상대적으로 처리속도가 빠릅니다. (split의 약 4배)

그리고 간단한 문자열을 처리하기에는 StringTokenizer를 사용하는것이 더 적합할 수 있습니다.

 

결론은 더 이상 StringTokenizer를 사용하지 말고 split 메소드를 사용하는게 현대적인 방법이라고 생각합니다.

 

개요

오늘은 Java 14부터 도입된 Record와 Java 15에서 preview로 시작된 Sealed Classes 에 대해서 알아보겠습니다.
참고로 Sealed Classes 는 Java 17부터 정식적으로 확정된 것같습니다.

 

JDK 17 Release Notes, Important Changes, and Information

These notes describe important changes, enhancements, removed APIs and features, deprecated APIs and features, and other information about JDK 17 and Java SE 17. In some cases, the descriptions provide links to additional detailed information about an issu

www.oracle.com

 
 

Record

Record는 불변(immutable) 데이터 객체를 생성하기 위한 새로운 유형의 클래스입니다. 주로 데이터를 운반하는 목적으로 사용되며, 기존의 많은 코드를 줄여줍니다.
 

Record 적용 전

public class Student {

    private final String name;
    private final int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

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

    @Override
    public String toString() {
        return "Student[name=" + name + ", age=" + age + "]";
    }

}

 

Record 적용 후

public record Student(String name, int age) { }

 

  • 모든 필드가 private final로 선언됩니다
  • 자동으로 equals(), hashCode(), toString() 메서드가 생성됩니다
  • 생성자와 접근자(getter)가 자동으로 생성됩니다
  • 상속이 불가능합니다

IntelliJ에서 record 를 추천하기도합니다.

 

    public static void main(String[] args) {
        Student student = new Student("철수", 20);

        System.out.println("student = " + student);
        System.out.println("student.name() = " + student.name());
        System.out.println("student.age() = " + student.age());
        
        // student = Student[name=철수, age=20]
        // student.name() = 철수
        // student.age() = 20
    }

 
 
 
 

Sealed Classes

Sealed Classes는 상속을 제한하는 새로운 방법을 제공합니다. 클래스나 인터페이스가 어떤 클래스에 의해 상속(extends)/구현(implements) 될 수 있는지 명시적으로 선언할 수 있습니다.
 

public sealed class Shape permits Circle, Rectangle, Square {
    // 공통 속성 및 메서드
}

public final class Circle extends Shape {
    private final double radius;
    // Circle 구현
}

public final class Rectangle extends Shape {
    private final double width;
    private final double height;
    // Rectangle 구현
}

public final class Square extends Shape {
    private final double side;
    // Square 구현
}

 
permits 한 클래스에서 sealed class를 구현하지 않거나, permits 에 명시되지않은 클래스에서 sealed 클래스를 구현하려고하면 에러가 발생합니다.
 

public sealed class Shape permits Circle, Rectangle, Square { // 에러!!
    // 공통 속성 및 메서드
}

public class Circle {
    private final double radius;
    // Circle 구현
}
public sealed class Shape permits Circle, Rectangle, Square {
    // 공통 속성 및 메서드
}

public final class Triangle extends Shape { // 에러!!

}

 

  • 상속/구현하는 클래스는 final, non-sealed, sealed 중 하나를 선언해야 합니다.
  • sealed class와 permit 된 subclass는 한 패키지 내에 존재해야 합니다.
  •  
키워드 설명
final 더 이상의 상속을 금지
sealed 제한된 상속
non-sealed 상속 허용

 

final

public final class Circle extends Shape {
    // Circle 구현
}

 

sealed

public sealed class Rectangle extends Shape permits ColoredRectangle {
    // Rectangle 구현
}

 

non-sealed

public non-sealed class Square extends Shape {
    // Square 구현
}

 
 

Sealed Classes 를 써야하는 이유가 있을까?

먼저 Sealed Classes를 도입하면 좋은 점을 알아보겠습니다.
 

타입 안정성 강화

  • 상속 가능한 클래스를 명확히 제한해서 타입 안정성을 높힙니다.
  • 컴파일 시점에서 가능한 모든 하위 타입을 알 수 있습니다.
  • switch 문법에서 하위 클래스가 모두 존재한다는 것을 컴파일러가 보장합니다. (switch JAVA 21 문법)

도메인 모델링 개선

  • 도메인 모델의 제약조건을 코드로 표현할 수 있습니다
  • 특정 타입이 가질 수 있는 모든 변형을 명시적으로 정의할 수 있습니다

 
sealed로 온라인 결제 시스템에서 지원하는 결제 수단을 예시로 만들어보겠습니다.

public sealed interface PaymentMethod permits 
    CreditCard, 
    BankTransfer, 
    DigitalWallet {
    
    boolean process(int amount);
    String getPaymentInfo();
}

public final class CreditCard implements PaymentMethod {
    private final String cardNumber;
    private final String expiryDate;
    private final String cvv;

    @Override
    public boolean process(int amount) {
        // 신용카드 결제 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Card: " + cardNumber.substring(12);
    }
}

public final class BankTransfer implements PaymentMethod {
    private final String accountNumber;
    private final String bankCode;

    @Override
    public boolean process(int amount) {
        // 계좌이체 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Bank: " + bankCode + "-" + accountNumber;
    }
}

public final class DigitalWallet implements PaymentMethod {
    private final String walletId;
    
    @Override
    public boolean process(int amount) {
        // 디지털 월렛 결제 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Wallet: " + walletId;
    }
}

// 결제 처리 서비스
public class PaymentService {
    public String processPayment(PaymentMethod method, int amount) {
    	// JDK 21 switch 문법
        return switch (method) {
            case CreditCard card -> 
                "신용카드 결제: " + card.getPaymentInfo();
            case BankTransfer transfer -> 
                "계좌이체: " + transfer.getPaymentInfo();
            case DigitalWallet wallet -> 
                "디지털 월렛: " + wallet.getPaymentInfo();
        }; // 모든 결제 수단이 처리됨을 컴파일러가 보장
    }
}

 

  • 지원하는 결제 수단을 명확하게 제한할 수 있습니다.
  • 새로운 결제 수단 추가 시 컴파일러가 관련된 모든 코드를 체크합니다.
  • switch 문에서 모든 케이스를 처리했는지 컴파일 시점에 확인할 수 있습니다.

 

개요

오늘은 객체 지향 프로그래밍(OOP)의 핵심 개념인 상속(Inheritance)과 복합(Composition)에 대해 설명해드리겠습니다. 이 두 개념은 코드 재사용과 유지보수성을 높이는 중요한 설계 방식입니다. 예제와 함께 특징을 살펴보며 공부해도록 하겠습니다.

 

 

 

상속

상속은 OOP에서 가장 기본개념중에 하나입니다. 부모 - 자식 클래스로 정의하여 자식클래스에서 부모 클래스의 메소드를 override, 재사용 할 수 있다는 특징이 있습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;

    public void start() {
        System.out.println("차량이 시동됩니다.");
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

 

public class ElectricCar extends Car {

    private int batteryCapacity;
    private int batteryLevel;

    public ElectricCar(String brand, String model, int year, int batteryCapacity) {
        super(brand, model, year);
        this.batteryCapacity = batteryCapacity;
        this.batteryLevel = 100;
    }

    @Override
    public void start() {
        System.out.println("전기차가 조용히 시동됩니다.");
    }

    public void charge() {
        System.out.println("배터리를 충전합니다.");
        this.batteryLevel = 100;
    }
    
}

 

ElectricCar 클래스는 Car 클래스를 상속받고있습니다. 따라서 Car와 ElectricCar는 부모-자식 관계입니다.

ElectricCar는 Car의 기능을 재사용하거나 새로운 기능을 손쉽게 추가할 수 있고, 때에 따라서 Override 할 수 있습니다. 그리고 Car 에서 변경된 사항은 ElectricCar에도 그대로 반영됩니다.

 

 

 

복합

@AllArgsConstructor
public class Engine {

    private final String type;
    private final int hp;

    public void start() {
        System.out.println(type + " 엔진이 가동됩니다.");
    }

}

 

@AllArgsConstructor
public class GasCar {

    private final Car car; // 복합관계
    private final Engine engine; // 복합관계

    public void start() {
        car.start();
        engine.start();
    }
}

 

Engine 클래스와 GasCar 클래스를 구현했습니다. GasCar는 Car와 Engine을 필드에서 참조하고있습니다.

이처럼 GasCar가 Car를 필드로 가지는 것이 복합관계라고합니다. GasCar를 통해 Car 메소드를 호출하는 방식이죠.

 

 

상속의 장단점

장점

  1. 코드 재사용성
    • 부모 클래스의 기능을 그대로 물려받아 사용할 수 있어 코드 중복을 줄일 수 있습니다.
    • 공통 기능을 부모 클래스에 구현하면 모든 자식 클래스가 활용할 수 있습니다.
  2. 간단한 확장성
    • 기존 클래스를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
  3. 다형성 구현
    • 부모 타입으로 자식 객체를 참조할 수 있어 유연한 프로그래밍이 가능합니다.
Car car = new ElectricCar("Tesla", "Model 3", 2024, 75);

 

단점

  1. 강한 결합도
    • 부모 클래스의 변경이 모든 자식 클래스에 영향을 미칩니다.
    • 부모 클래스의 내부 구현을 자식 클래스가 알아야 할 수 있습니다.
  2. 취약한 기반 클래스 문제
    • 부모 클래스의 변경이 예상치 못한 자식 클래스의 동작 변경을 일으킬 수 있습니다.
  3. 단일 상속 제한
    • Java는 다중 상속을 지원하지 않아 한 클래스만 상속할 수 있습니다.

 

복합의 장단점

장점

 

  1. 유연한 설계
    • 런타임에 구성요소를 변경할 수 있습니다.
    • 다중 복합이 가능합니다.
  2. 캡슐화 향상
    • 내부 구성요소의 구현을 완전히 숨길 수 있습니다.
    • 인터페이스를 통한 통신이 가능합니다.
  3. 느슨한 결합도
    • 각 클래스가 독립적으로 동작하여 유지보수가 용이합니다.
// Engine을 쉽게 교체할 수 있음
public void updateEngine(Engine newEngine) {
    this.engine = newEngine;
}

 

 

 

단점

  1. 코드 가독성
    • 복잡한 구성의 경우 코드 추적이 어려울 수 있습니다.
  2. 구현의 복잡성
    • 여러 클래스를 조합하여 사용하므로 초기 설계가 더 복잡할 수 있습니다.
// 복합 관계에서는 위임 메서드를 많이 작성해야 할 수 있음
public class GasCar {
    private Car car;
    private Engine engine;
    
    public void start() { car.start(); }
    public void stop() { car.stop(); }
    public void accelerate() { engine.accelerate(); }
    // ... 더 많은 위임 메서드들
}

 

 

결론

상속과 복합에 대한 글을 읽다보면 'IS-A' 일때는 상속, 'HAS-A' 일때는 복합을 사용하라는 글이 많습니다.

'전기차는 차다' -> 상속,

'차는 엔진을 갖고있다' -> 복합,

이렇게 이해하면 조금더 와닿을것 같습니다.

 

최근의 객체지향 설계에서는 "상속보다는 복합을 사용하라"는 원칙이 널리 받아들여지고 있습니다. 복합이 더 유연하고 안전한 설계를 제공하기 때문입니다. 하지만 각각의 상황에 맞는 적절한 선택이 중요합니다.

 

마지막으로 위의 예제를 상속과 복합관계를 이용하여 CasCar를 조금 더 깔끔하게 바꿔보면서 글을 마치겠습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;
    private final Engine engine;

    public void start() {
        System.out.println("차량이 시동됩니다.");
        engine.start();
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

public class GasCar extends Car {

    private final int fuelCapacity;
    private double currentFuel;

    public GasCar(String brand, String model, int year, Engine engine, int fuelCapacity) {
        super(brand, model, year, engine);
        this.fuelCapacity = fuelCapacity;
        this.currentFuel = fuelCapacity;
    }

    public void refuel(double amount) {
        currentFuel = Math.min(currentFuel + amount, fuelCapacity);
    }

}

 

차는 엔진을 가지고있다 -> private final Engine engine

가솔린차는 차이다 -> GasCar extends Vehicle

 

Spring Scheduler

스케줄러는 스프링 프레임워크에 포함된 기능입니다. 특정 시간이나 정해진 주기에 따라 작업을 수행하는데 사용하는데 오늘은 이 기능에 대해서 알아봅시다.

 

 

 

사용방법

 

설정

Spring Scheduler를 사용하기 위해서는 @EnableScheduling 어노테이션을 선언해주어야 합니다.

@EnableScheduling
@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

 

 

구현

다음은 스케줄을 구현하면되는데 스프링 빈으로 등록되어야지 동작합니다.

@Component
public class TestSchedule {

    @Scheduled(fixedRate = 1000)
    public void test() {
        System.out.println("TestSchedule");
    }

}

 

 

실행해보면 1초간격으로 동작하는것을 확인할 수 있습니다.

 

 

 

@Scheduled 사용법

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
@Reflective
public @interface Scheduled {
    String CRON_DISABLED = "-";

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1L;

    String fixedDelayString() default "";

    long fixedRate() default -1L;

    String fixedRateString() default "";

    long initialDelay() default -1L;

    String initialDelayString() default "";

    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    String scheduler() default "";
}

 

  1. cron
    • 표현식을 이용해 스케줄을 지정할 수 있습니다. 
    • "초 분 시 일 월 요일" 형식으로 작성합니다.
    • 자세한 설명을 글 하단에서 하겠습니다.
  2. zone
    • cron 표현식에서 사용할 시간대를 지정합니다.
    • default 는 서버의 로컬 시간대입니다.
    • "Asia/Seoul", "UTC" 등
  3. fixedDelay, fixedDelayString
    • 이전 작업의 종료시점부터 다음 작업의 시작까지의 지연시간입니다.
    • default fixedDelay = 1000 : 1초 (이전 작업 완료 후 1초 뒤 실행)
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  4. fixedRate, fixedRateString
    • 이전 작업의 시작지점부터 다음 작업의 시작까지의 시간간격입니다.
    • default fixedRate = 1000 : 1초 (이전 작업 시작으로부터 1초 마다 실행)
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  5. initialDelay, initalDelayString
    • 애플리케이션 시작 후 첫 작업이 실행되기까지의 지연시간을 지정할 수 있습니다.
    • default는 위와 같습니다.
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  6. timeUnit
    • fixedDelay, fixedRate, initialDelay의 시간단위를 지정합니다.
    • default TimeUnit.MILLISECONDS
  7. scheduler
    • 스케줄러의 이름을 지정할 수 있습니다.
@Scheduled(cron = "0 0 12 * * ?")  // 매일 12시에 실행
public void test1() {
    // 작업 내용
}

@Scheduled(fixedRate = 5000)  // 5초마다 실행
public void test2() {
    // 작업 내용
}

@Scheduled(fixedDelay = 1000, initialDelay = 5000)  // 시작 5초 후 첫 실행, 이후 1초 간격으로 실행
public void test3() {
    // 작업 내용
}
  •  

 

 

 

cron

cron은 "초 분 시 일 월 요일" 형식으로 된 String 문자열입니다.

필드 허용값
0 - 59
0 - 59
0 - 23
1 - 31
1- 12 | JAN-DEC
요일 0 - 6 | SUN - SAT

 

cron 특수문자

특수문자 설명 예제
* 모든 값  
? 특정한 값이 없음 일, 요일에서만 사용가능
- 범위를 나타냄 1-3 : 1월 ~ 3월
, 값을 여러개 나열 MON,SAT,SUN : 월,토,일
/ 시작시간과 단위 분에서 1/5 : 1분부터 5분단위로
L 마지막 값 일, 요일에서만 사용가능
일에서 사용하면 마지막 일,
요일에서 사용하면 마지막 요일(6)
W 가장 가까운 평일 일 에서만 사용가능
1W : 1일에서 가장 가까운 평일
# 몇째주의 무슨요일 0#3 : 3번째 주 일요일

 

 

 

cron 예제

// 매일 자정에 실행
@Scheduled(cron = "0 0 0 * * ?")

// 평일 오전 9시에 실행
@Scheduled(cron = "0 0 9 * * MON-FRI")

// 매월 1일 오전 3시에 실행
@Scheduled(cron = "0 0 3 1 * ?")

// 매주 일요일 자정에 실행
@Scheduled(cron = "0 0 0 ? * SUN")

// 매월 마지막 날 오후 11시 45분에 실행
@Scheduled(cron = "0 45 23 L * ?")

// 매월 마지막 토요일 오후 10시에 실행
@Scheduled(cron = "0 0 22 ? * 6L")

// 매월 첫번째 화요일 오전 9시에 실행
@Scheduled(cron = "0 0 9 ? * 2#1")

// 매일 오전 9시부터 오후 5시까지 30분마다 실행
@Scheduled(cron = "0 0/30 9-17 * * ?")

// 매일 10시 15분에 실행
@Scheduled(cron = "0 15 10 * * ?")

// 매주 월요일 10시에 실행
@Scheduled(cron = "0 0 10 ? * MON")

// 매주 월,수,금 10시에 실행
@Scheduled(cron = "0 0 10 ? * MON,WED,FRI")

// 15분마다 실행
@Scheduled(cron = "0 0/15 * * * ?")

// 평일 9시-18시 정각마다 실행
@Scheduled(cron = "0 0 9-18 * * MON-FRI")

// 매월 마지막날 12시에 실행
@Scheduled(cron = "0 0 12 L * ?")

// 매월 마지막 평일 12시에 실행
@Scheduled(cron = "0 0 12 LW * ?")

 

 

설정분리

schedule.cron=3 * * * * ?
schedule.enabled=true

application.properties에 설정값을 분리하고

 

@Component
public class TestSchedule {

    @Value("${schedule.enabled}")
    private boolean enabled;

    @Scheduled(cron = "${schedule.cron}")
    public void test() {
        if (enabled) {
            System.out.println("TestSchedule");
        }
    }

}

 

이렇게 작성하면 설정부분과 실행부분을 나눌 수 있어서 좋은것 같습니다.

개요

Spring JPA에는 open-in-view 를 true / false 로 설정할 수 있습니다.

spring.jpa.open-in-view=true // true : default

오늘은 이 기능이 무엇을 하는지 간단하게 알아보도록 합시다.

 

 

 

 

OSIV

open-in-view(OSIV) 는 영속성 컨텍스트가 데이터베이스 커넥션을 언제 돌려주는지 설정하는 것입니다.

OSIV의 기본전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 까지 영속성 컨텍스트

데이터베이스 커넥션을 유지하는 것입니다. 즉, 트랜잭션이 종료되어도 데이터베이스 커넥션을 유지하는 것인데요.

이 전략은 Controller에서 지연로딩을 통해 데이터를 받아올 수 있다는 장점이 있습니다.

다만, 이 기본전략은 데이터베이스 커넥션을 필요 이상으로 오래 사용하고, 오래 사용하기 때문에 트래픽이 많은 어플리케이션에서는 커넥션이 모자라 문제를 일으킬 수 있다는 것입니다.

 

 

spring.jpa.open-in-view=false

따라서 OSIV를 false로 하여 커넥션 반환시점을 트랜잭션 종료시점과 맞추어 사용하는 것이 조금더 나을 수 있습니다.

'FrameWork > JPA' 카테고리의 다른 글

[JPA] default_batch_fetch_size  (0) 2024.11.08
JPA 스키마 자동 생성  (0) 2023.11.17

퍼사드 패턴

퍼사드 패턴은 복잡한 클래스들을 편하게 사용하기위해 인터페이스를 구성하는 구조 패턴입니다.

 

 

 

예제

IOT 를 조작하는 컨트롤러를 만든다고 가정해보겠습니다.

컨트롤러는 일어날때 커튼을 열고, 전등을 키고, 스피커에서 음악이 재생되도록 하고싶고,

집을 떠날때는 전등을 끄고, 스키퍼에서 음악이 멈추도록 하고싶습니다.

 

public class Remote {

    private final Lights lights;
    private final Curtains curtains;
    private final Speaker speaker;

    public Remote(Lights lights, Curtains curtains, Speaker speaker) {
        this.lights = lights;
        this.curtains = curtains;
        this.speaker = speaker;
    }

    public void wakeUp() {
        lights.on();
        curtains.open();
        speaker.musicStart();
    }

    public void leaveHome() {
        lights.off();
        speaker.musicStop();
    }
    
}
public class Lights {

    public void on() { ... }
    public void off() { ... }
    
}

public class Curtains {

    public void open() { ... }
    public void close() { ... }
    
}

public class Speaker {

    public void musicStart() { ... }
    public void musicStop() { ... }
    
}

 

이렇게 작성한다면 Remote를 사용하는 사용자는 내부 클래스의 구현에 대해서 알 필요가 없어지고, 내부 클래스의 수정이 일어나도 Remote 코드만 수정하면 되므로 유연성이 향상됩니다.

 

 

이 퍼사드 패턴은 디자인패턴에 대해서 알지 못하더라도 그냥 자연스럽게 만들어지는 패턴같습니다. 쉽고 유용한 패턴이니 잘 적용해보시기 바랍니다.

spring.jpa.properties.hibernate.default_batch_fetch_size

application.properties

 

default batch fetch size

JPA에서 default_batch_fetch_size 는 지연로딩되는 쿼리를 IN 으로 묶어 한번에 보내는 기능입니다. 조회 시 성능을 개선하는 방법중 하나죠.

 

 

 

List<Order> orders = orderRepository.findAll();
orders.forEach(order -> System.out.println(order.getMember().getName()));

예를들어, 위와 같은 로직이 있을때 orders의 사이즈가 100이고, Member는 fetch=LAZY 이라고 가정해보겠습니다.

 

order.getMember().getName()을 호출하면 영속성 컨텍스트에 데이터가 없기때문에 먼저 쿼리가 나가고 영속성 컨텍스트에 저장됩니다. Member의 중복이 없다면 이 과정에서 100번 반복되어 총 쿼리는 최대 101번이 나갈 수 있게되는것입니다.

 

여기에서 default_batch_fetch_size=100 라고 설정해준다면 지연로딩되는 member를 IN절로 100개까지 묶어 한번에 모두 가져와 컨텍스트에 넣게됩니다. 그러면 이 경우에 2번의 쿼리만 발생하게됩니다.

 

따라서 default_batch_fetch_size를 적절하게 설정해놓는것이 좋겠습니다.

'FrameWork > JPA' 카테고리의 다른 글

[JPA] Open Session In View  (0) 2024.11.10
JPA 스키마 자동 생성  (0) 2023.11.17

+ Recent posts