🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [41]일차
🚀41일차에는 람다식과 스트림 API에 대해 배울 수 있었다.
이전에 자바스크립트에서 화살표함수를 사용했을때나 알고리즘 문제 풀이 시 접했던 람다식과 스트림이어서 기초를 잘 배워놔야겠다고 생각했다.
특히 스트림의 경우 데이터를 다루는 다양한 메소드를 제공하므로 더 자세히 공부해야겠다고 생각했다.
Spring JDBC 와 Spring Data JDBC의 차이점
Spring JDBC
- JDBC(Java Database Connectivity) API를 더 편리하게 사용할 수 있도록 도와주는 DB 접근 기술
- SQL 쿼리를 직접 작성 및 (ResultSet, PreparedStatement, Connection)과 같은
JDBC 객체들을 관리하는 것을 도와줌 - Spring JDBC의 핵심 클래스로 JdbcTemplate가 있다. (보통 JdbcTemplate 클래스를 중심으로 이루어짐)
- Spring JDBC의 핵심 메소드로 queryForObject(), query(), update() 등이 있고 CRUD 연산을 수행 가능
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void run(String... args) throws Exception {
String sql = "INSERT INTO users(name,email) VALUES(?,?)";
jdbcTemplate.update(sql, "Watkins", "Watkins@premier.com");
}
- JdbcTemplate 클래스를 직접 사용한다는 특징
- SQL 쿼리를 직접 작성했다는 특징
Spring Data JDBC
- Spring Data의 확장 개념으로 Spring Data 프로젝트의 하위 모듈
- JDBC를 더 추상화하여 ORM(객체 관계 매핑)를 도와줌
- Spring Data JDBC는 JPA와 유사하지만, 더 단순한 구조로 동작
- CrudRepository or JdbcRepository 인터페이스 상속으로 CRUD 로직 자동화 (보통 Repository 기반으로 이루어짐)
- 기본적인 CRUD 연산 관련 쿼리는 자동으로 생성하여 도와줌
- 복잡한 ORM을 대체할 수 있는 기술
➡️@Table, @Id 같은 어노테이션으로 ORM 매핑이 가능하여 DB 연결이 단순해짐
// 1. 도메인 모델
@Table("users")
public class User {
@Id
private Long id;
private String name;
private String email;
// 생성자, getter/setter 생략
}
// 2. Repository 인터페이스
public interface UserRepository extends CrudRepository<User, Long> {}
// 3. Application 클래스
@SpringBootApplication
public class SpringDataJDBCApplication implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
public static void main(String[] args) {
SpringApplication.run(SpringDataJDBCApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
User user = new User(null, "Watkins", "Watkins@premier.com");
userRepository.save(user); // SQL 작성을 하지 않아도 됨
}
}
- Repository 인터페이스와 Domain Model을 활용한다는 특징
- SQL 쿼리를 직접 작성하지 않음
➡️CRUD 연산 자동화 (save(), findById(), delete() 등)
정리하자면
Spring JDBC | Spring Data JDBC | |
SQL 쿼리 | 개발자가 직접 작성 | 기본적인 CRUD 연산은 자동 수행 |
추상화 | 낮음 | 높음 (JPA와 유사하여 간결함) |
- 이 둘을 구분하는 기준은 JdbcTemplate 클래스 사용 유무보다
(Spring Data JDBC또한 내부적으로 JdbcTemplate이 동작하고 있기때문) - "Repository 패턴의 사용 여부"로 판단할 수 있음
람다식 Lambda
- 자바 8부터 도입되었음
- 메소드를 화살표 연산자를 활용하여 하나의 식으로 표현한 것
- 자바스크립트는 함수를 객체로 취급하지만 자바는 함수를 객체로 취급하지 않음
(=자바는 메소드가 리턴되고 메소드가 매개변수로 들어가는 경우가 없다는 것) - 이름없이 사용할 수 있어 코드의 간결성이 높아짐
- 이때 람다식을 이용하면 메소드를 함수처럼 쓰지만 객체로 만들어서 넣어주게된다.
➡️람다 미사용 : 익명객체를 만들어서 만든 객체를 매개변수에 넣어주어야하는 부분 성능 저하 발생
➡️람다 사용 : 컴파일러가 알아서 람다식을 익명객체로 만들어줌
// 1. 람다가 아닐때 (메소드)
int sum(int a, int b){
return a + b;
}
// 2. 람다식으로 변경
(int a, int b) -> a + b
- 간단히 표현하면 int같은 매개변수의 타입을 생략 가능 ➡️(a, b) -> a + b
- 컴파일러가 자동으로 찾아주기때문에 생략 가능
- 단일 표현식일 경우에는 return부까지 생략 가능
- 람다식은 함수형 인터페이스 (추상메소드가 1개만 존재)구현할때 사용
➡️@FunctionalInterface 어노테이션 활용하여
이벤트처리, 콜백함수 작성, 서비스 레이어의 로직을 간소화 하는 등에 활용
▶️실습 - Runnable 사용을 통한 람다식 비교
1. 일반 Runnable 과 람다식 Runnable
// 1. 일반 Runnable 선언법
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("[일반 Runnable] = Hello!!!");
}
};
new Thread(runnable).start();
// 2. 람다식 Runnable 선언법
Runnable runnable2 = () -> System.out.println("[람다식 Runnable] = Hello!!!");
new Thread(runnable2).start();
- 쓰레드 객체는 Runnable이라는 인터페이스를 받아들이도록 함
(ex. new Thread(runnable)) - 이를 람다식으로 대체할 수 있다는 것
ex. Iterator 인터페이스를 리스트에서 사용시
Iterator 인터페이스를 구현한 객체가 내부적으로 동작하여 기능을 하는 것과 유사
2. 별도 클래스의 Runnable
public class MyRunnable implements Runnable{
// 3. 별도의 클래스로 분리한 Runnable 선언법
@Override
public void run() {
System.out.println("[별도의 클래스 Runnable] = Hello!!!");
}
}
▶️실습 - List에서의 람다식
1. 일반 List 사용 - 정렬에 Comparator 객체 사용
// 리스트에서의 람다식
public class exam02 {
public static void main(String[] args) {
// 1. 일반 List 선언법
List<String> names = new ArrayList<>();
names.add("Aaron");
names.add("Curtis");
names.add("Bradley");
names.add("Dejan");
// 1. 일반 List 정렬 (Comparator 사용)
names.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println(names);
}
}
- names.sort(new Comparator<String>() {...}
➡️sort() 메소드는 Comparator 인터페이스를 인자로 받음 - new Comparator<String>()
➡️익명 클래스를 생성하여 Comparator를 구현하는 객체를 만듦
이후 Comparator 객체의 추상 메소드인 compare()를 오버라이딩함 - return o1.compareTo(o2);
➡️o1과 o2를 사전 순 비교 후 정렬함
➡️compareTo() 메소드는
o1 == o2 이면 0
o1 > o2 이면 양수
o1 < o2 이면 음수를 반환
2. 람다식 List 사용
// 2. 간결한 List 선언법
List<String> nameList = Arrays.asList("Dejan", "Bradley", "Aaron", "Curtis");
// 2. 간결한 List의 정렬 (람다식 사용)
nameList.sort((o1, o2) -> o1.compareTo(o2));
System.out.println(nameList);
- 둘다 잘 정렬된 것을 확인할 수 있다.
3. 메서드 참조 사용
names.sort(String::compareTo);
- (o1, o2) -> o1.compareTo(o2)를 메소드 참조를 사용하여 더 간결히 표현 가능
- String 클래스의 compareTo 메서드를 참조한 것
정리하자면
(o1, o2) -> o1.compareTo(o2)
이 람다식 코드가
new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
new Comparator 객체를 대신한다는 것이다.
- 즉 자바에서 람다식은 익명 클래스를 간결하게 표현하기 위한 기능이다.
- 특히 Comparator와 같은 함수형 인터페이스 구현 시 불필요한 코드 작성 없이
람다식으로 핵심 로직만을 간결하게 표현 가능
람다 사용 X | 람다 사용 O |
new Comparator<String>() { | (o1, o2) -> 에 해당 |
public int compare(String o1, String o2) | (o1, o2) 에 해당 |
return o1.compareTo(o2); | o1.compareTo(o2) 에 해당 |
▶️실습 - 사용자 정의 Comparator (MyComparator)
public class MyComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
// 1. 일반 List 선언법
List<String> names = new ArrayList<>();
names.add("Aaron");
names.add("Curtis");
names.add("Bradley");
names.add("Dejan");
// 1. Comparator 사용을 간단히 하기 위해 사용자 정의 Comparator 사용
MyComparator myComparator = new MyComparator();
names.sort(myComparator);
nameList.sort((o1, o2) → o1.compareTo(o2));
forEach() 메소드
- forEach()메소드는 매개변수로 Consumer를 받음
1. forEach() + Consumer 객체 - 람다 미사용
List<String> names = Arrays.asList("Baines", "McGinn", "Digne");
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
names.forEach(consumer);
- Consumer 인터페이스는 추상메소드 accept()를 한 개 가지고 있음
- System.out.println(s)
➡️names 리스트의 각 요소들이 줄바꿈 출력됨
- 이를 람다식 (화살표 활용)으로 바꿔서 간단하게 사용 가능
2. forEach() + Consumer 객체 - 람다 사용
// 2. 람다식 사용 forEach()
names.forEach(name -> System.out.println(name));
- 람다식을 활용하여 accept()메소드가 포함된 Consumer 객체를 직접 만들지 않아도
람다식을 통해 메소드의 기능을 바로 사용 - 💡Consumer 인터페이스의 추상메소드 accept()는 "매개변수 하나를 받아서 리턴타입 없이 수행" 하는 메소드이므로람다식에서 기능으로써 바로 사용 가능한 것
3. forEach() + Consumer 객체 - 메소드 참조 사용 ( :: )
// 3. 람다식 활용 개선 forEach()
names.forEach(System.out::println);
- System.out::println
➡️컴파일러가 자동으로 Consumer객체를 구현하여 메소드 수행 후 결과를 반환 (=출력)
▶️실습 - List 사용에서의 람다식, 메소드 참조 사용
// 2. 간결한 List 선언법
List<String> nameList = Arrays.asList("Dejan", "Bradley", "Aaron", "Curtis");
// 2. 간결한 List의 정렬 (람다식 사용)
nameList.sort((o1, o2) -> o1.compareTo(o2));
System.out.println(nameList);
// 3. 람다식 사용 개선
nameList.sort(String::compareTo);
- "메소드 참조 (Method Reference) (혹은 메소드 레퍼런스)"를 사용
❓자바 람다식과 자바스크립트 화살표함수의 차이점
크게 보면 자바의 람다식은 함수(=메소드)가 객체로 바뀌어서 들어가지만
자바스크립트의 화살표함수는 함수가 그대로 들어간다는 차이점
자바의 람다식
- 함수가 객체로 변환됨
➡️함수형 인터페이스의 구현체(=객체)로 변환됨 - OOP (객체지향프로그래밍) 기반
람다식에 들어올 수 있는 인터페이스는 (반드시 추상메소드 하나만 존재해야한다)는 조건이 있다.
이는 함수형 인터페이스의 특징으로 @FunctionalInterface 어노테이션을 통해 명시 가능.
즉 단 하나의 추상 메소드만 가져야 하고 그 외로 default 메소드나 static 메소드는 여러 개 포함될 수도 있음
자바스크립트의 화살표 함수
- 자바스크립트의 화살표 함수는 "일급 객체"
➡️화살표 함수 자체가 하나의 값으로 취급되어 변수에 저장되거나, 인자로 전달될 수 있음 - Functional 기반 - 함수 자체가 값
const greet = () => console.log("Hello, JavaScript!");
greet(); // 함수 호출
@FunctionalInterface
- 함수형 인터페이스는 메소드가 한개만 존재해야함
1. 에러 발생
@FunctionalInterface
public interface MyFunctionalInterface {
public void method1();
public void method2();
}
- 이처럼 메소드가 method1(), method2() 두 개가 쓰이면 @FunctionalInterface 어노테이션에 오류가 발생할 것
2. 올바른 사용
@FunctionalInterface
public interface MyFunctionalInterface {
public void method1();
}
- 이처럼 추상메소드가 하나만 존재해야할 것
- @FunctionalInterface 어노테이션을 통해 함수형 인터페이스임을 명시하고
함수형 인터페이스의 역할을 하도록 역할을 고정 가능 - 어노테이션은 반드시 명시할 필요는 없지만 메소드가 한 개뿐인 함수형 인터페이스임을 표시하는 기능
▶️실습 - 함수형 인터페이스
1. 함수형 인터페이스 - 람다식 사용
public static void main(String[] args) {
MyFunctionalInterface myFunc1;
myFunc1 = new MyFunctionalInterface() {
@Override
public void method(int x) {
int result = x * 5;
System.out.println("5를 곱한 결과 : " + result);
}
};
// 1. 함수형 인터페이스 myFunc1의 메소드 호출
myFunc1.method(5);
// 2. 람다식으로 myFunc2 메소드 호출
MyFunctionalInterface myFunc2;
// MyFunctionalInterface의 메소드인 method()는 int타입 x를 받아들이므로
// 타입을 생략하여도 컴파일러가 int타입으로 인지한다.
myFunc2 = x -> {
int result = x * 5;
System.out.println("5를 곱한 결과 : " + result);
};
}
2. 함수형 인터페이스 - 인터페이스 분리
// 3. 메소드 분리 사용
public static void testMethod(MyFunctionalInterface MyFunc3, int number){
MyFunc3.method(number);
}
public static void main(String[] args) {
// 3. 메소드 분리 사용
testMethod(a -> {
int result = a * 3;
System.out.println("결과 : " + result);
}, 6);
}
- (MyFunctionalInterface MyFunc3, int number)
➡️MyFunctionalInterface타입의 MyFunc3 매개변수에 람다식이 할당될 수 있음 (=메소드를 전달받기위한 자리)
➡️int number : 정수형 매개변수로 람다식 내부에 전달될 수 있도록 하는 변수 - MyFunc3.method(number);
➡️전달받은 MyFunc3 객체의 method() 메소드를 호출할때 number를 인자로 전달함 - testMethod(a -> { ... }, 6);
➡️testMethod() 메소드 호출 시 두 개의 인자를 전달하는데,
첫번째 인자 : a -> { int result = a * 3; System.out.println("결과 : " + result);
두번째 인자 : 6
을 전달하게 된다. - a -> { int result = a * 3; System.out.println("결과 : " + result);
➡️MyFunctionalInterface의 method(int a)를 구현하는 람다식
➡️a : method() 메소드의 매개변수 int a를 의미, 6이 전달됨
이로써 int result = a * 3; 의 결과는 6 * 3으로 18이 result에 할당됨 - 1, 2, 3번 방법 모두 사용이 가능
▶️실습 - 덧셈 수행 함수형 인터페이스
public static void main(String[] args) {
MyFunctionalInterface02 MyFunc;
MyFunc = new MyFunctionalInterface02() {
@Override
public int method(int x, int y) {
return x + y;
}
};
MyFunctionalInterface02 MyFuncLambda;
MyFuncLambda = (x, y) -> x + y;
// 1. 기본적인 람다식 사용방법
System.out.println(MyFuncLambda.method(50, 51));
}
▶️실습 - 람다식의 인자를 직접 입력하여 전달
Scanner sc = new Scanner(System.in);
System.out.print("첫 번째 숫자 입력: ");
int x = sc.nextInt();
System.out.print("두 번째 숫자 입력: ");
int y = sc.nextInt();
// 매개변수 이름을 a, b로 변경
MyFunctionalInterface02 MyFuncLambda = (a, b) -> a + b;
System.out.println("결과: " + MyFuncLambda.method(x, y));
- 이처럼 람다식에서의 변수는 a, b를 사용하여 람다식의 사용을 선언해주고
- 실제 method()를 호출하는 부분에서는 Scanner를 통해 입력받은 변수 x, y를 a, b자리에 대치시켜줌으로써 수행가능
▶️실습 - 사용자 정의 함수형 인터페이스 - 계산
@FunctionalInterface
public interface IntBinaryOperation {
int apply(int a, int b);
}
- 두 정수를 받아 처리하고 정수를 반환하는 하나의 추상 메소드 apply를 가진 함수형 인터페이스 정의
// 1. 람다식 사용 X
IntBinaryOperation normalAdd;
normalAdd = new IntBinaryOperation() {
@Override
public int apply(int a, int b) {
return a + b;
}
};
// 2. 람다식 사용 O
IntBinaryOperation add = (a, b) -> (a + b);
IntBinaryOperation add = (a, b) -> (a + b);
IntBinaryOperation subtract = (a, b) -> (a - b);
IntBinaryOperation multiply = (a, b) -> (a * b);
IntBinaryOperation divide = (a, b) -> (a / b);
System.out.println("람다식 덧셈 : " + add.apply(10, 5));
System.out.println("람다식 뺄셈 : " + subtract.apply(30, 17));
System.out.println("람다식 곱셈 : " + multiply.apply(20, 20));
System.out.println("람다식 나눗셈 : " + divide.apply(99, 3));
- IntBinaryOperation 인터페이스 타입으로 만들고 add, subtract... 등으로 객체를 만드는데
그 객체는 (a, b) -> (a + b); 처럼 람다식으로 간단하게 표현 가능하다. - 이 람다식을 호출할때는 인터페이스에 정의되어있는 추상메소드 apply()를 사용하여
인자 2개의 값을 넣고 계산이 가능하다.
람다와 final
- 변수 범위의 람다
- 람다식 내에서 변수에 접근할때는 특별한 규칙을 따른다.
➡️람다식은 자신을 둘러싸는 영역의 지역 변수에 접근할 수 있지만
이 변수가 반드시 final이거나 사실상 final인 변수여야한다는 것이다. - ❓사실상 final : 변수가 final로 선언되지 않았어도 해당 변수에 대한 재할당이 발생하지 않을때를 의미
- final 사용의 이유 :
➡️람다식이 실행되는 시점에는 람다식을 둘러싼 메소드가 이미 실행을 마치고
그 안에 있던 지역 변수가 이미 소멸되었을 수 있다.
즉 람다식은 복사된 변수 값으로 작동하기 때문에 변수의 값이 변경되지 않는다는 가정하에 안전하게 작동해야함
➡️이를 통해 람다식의 일관성과 예측가능성을 유지함
또한 함수형 프로그래밍의 핵심 개념인 “불변성”을 지원함
▶️실습 - 람다와 final
public static void main(String[] args) {
int x = 10;
Runnable r = () -> {
System.out.println("x : " + x);
};
// 이 경우 람다식 안에서 쓰였던 x가 변경될 수 있는 가능성이 존재하므로 오류가 발생
// x = 20;
r.run();
}
- 변수 x가 사실상 final임 (=더이상 변수의 값이 재할당되지 않는 경우)
람다와 자바 표준 API
- 자바에서 람다식 사용은 표준 API 의 여러 함수형 인터페이스를 활용해 확장됨
- 주요 함수형 인터페이스
➡️Consumer<T> : 하나의 입력을 받고 반환값이 없는 작업 수행 (accept(T t))
➡️Supplier<T> : 아무런 입력 없이 값을 반환 (get())
➡️Function<T, R> : 하나의 입력을 받고 결과를 반환 (apply(T, t))
➡️Predicate<T> : 하나의 입력에 대해 boolean 값 반환 (test(T, t)), (=조건에 따른 요소 필터링)
Predicate<T>
// 입력을 받아서 결과로 boolean 리턴
// 조건을 테스트할때 사용 (ex. 음수/양수 체크 메소드)
Predicate<Integer> isPositive = x -> x > 0;
System.out.println(isPositive.test(4)); // true 출력
System.out.println(isPositive.test(-1)); // false 출력
- 즉 람다식의 구조가 x → x > 0이면 (화살표의 왼쪽 식이 입력값 받기, 화살표의 오른쪽 식이 조건식 체크)
Consumer<T>
// Consumer : 입력을 받아서 반환값이 없는 연산 수행
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Step 1. Lambda - [Hello Consumer Test]");
Consumer<String> printer2 = System.out::print;
printer2.accept("Step 2. Simplizing - [Hello Consumer Test]");
- Consumer인터페이스는 andThen() 이라는 디폴트 메소드가 있다.
(화살표의 왼쪽 식이 입력값 받기, 화살표의 오른쪽 식이 출력)
Consumer의 andThen()
// Consumer의 andThen()
Consumer<String> conA = s -> System.out.println(s + " [Vardy]!!!");
Consumer<String> conB = s -> System.out.println(s + " [Zinchenko]!!!");
conA.accept("\\n\\n[Normal 1] My Name is");
conB.accept("[Normal 2] My Name is");
Consumer<String> conAB = conA.andThen(conB);
conAB.accept("[andThen()] My Name is");
- 각각을 수행할 필요없이 conA.andThen(conB)처럼 연결해놓으면
- conAB.accept() 수행 시 conA.accept() 수행 후 conB.accept()수행을 자동으로 해준다.
Function<T>
// Function : 입력을 받아 연산 후 출력
Function<String, Integer> lengthFunc = s -> s.length();
System.out.println("\\nStep 1. Lambda - 문장 길이 : " + lengthFunc.apply("Brentford!!"));
lengthFunc = String::length;
System.out.println("Step 2. Simplizing - 문장 길이 : " + lengthFunc.apply("Brentford!!"));
BiFunction
// BiFunction : 두 개의 값을 입력받아, 하나의 결과값만 출력
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println("\\nStep 1. Lambda (BiFunction) 덧셈 : " + add.apply(10, 15));
- 두 개의 Integer를 받아 하나의 Integer를 반환하는 함수형 인터페이스
Supplier<T>
- 주로 필요 시에만 객체 생성 및 지연 초기화에 사용
- ex. Stream API 같은 함수형 프로그래밍 패턴에서 많이 활용
// Supplier : 입력없이 값을 반환
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println("Step 1. Lambda (Supplier) - 랜덤 값 : " + randomSupplier.get());
randomSupplier = Math::random;
System.out.println("Step 1. Simplizing (Supplier) - 랜덤 값 : " + randomSupplier.get());
IntSupplier<T> : int반환값만 반환해주는 것
// IntSupplier : 입력없이 int 값을 반환
IntSupplier intSupplier = () -> (int)(Math.random()*6)+1;
System.out.println(intSupplier.getAsInt());
메소드 참조 ( :: )
1. 정적 메소드 및 인스턴스 메소드 참조
// 메소드 참조 (:: 를 사용)
// 1. 정적 메소드 참조
BiFunction<Integer, Integer, Integer> maxFunc = Math::max;
System.out.println("[정적 메소드 참조] 최대값 : " + maxFunc.apply(4, 7));
// 2. 인스턴스 메소드 참조
String str = "Everton - Aaron Lennon";
Supplier<Integer> lengthFunc = str::length;
// str이 가진 length()를 리턴해달라는 표시를 메소드 참조를 통해 간결히 표현 가능
System.out.println("[인스턴스 메소드 참조] 문자열 길이 : " + lengthFunc.get());
- 1. 정적 메소드 참조
apply(4, 7) 호출
➡️내부적으로 Math.max(4, 7)이 실행
➡️즉, maxFunc.apply(4, 7) == Math.max(4, 7) - 2. 인스턴스 메소드 참조
- str::length
➡️str.length() 메소드를 참조한 것
- Supplier<Integer>
➡️매개변수 없이 Integer 타입의 값을 반환하는 함수형 인터페이스
- lengthFunc.get() 호출
➡️str.length()가 실행되어 문자열의 길이를 반환하게됨
2. 임의 객체의 인스턴스 메소드 및 생성자 참조
// 3. 임의 객체의 인스턴스 메소드 참조
List<String> players = Arrays.asList("Walcott", "Watkins", "Bradley", "Jones");
List<Integer> nameLength = new ArrayList<>();
Function<String, Integer> playerNameLength = String::length;
for(String player : players){
nameLength.add(playerNameLength.apply(player));
}
System.out.println(nameLength);
- for (String player : players) { nameLength.add( playerNameLength.apply(player)); }
➡️players 리스트를 순회하면서 각 이름에 대해 playerNameLength.apply(player)를 호출
내부적으로는 player.length()가 동작
결과적으로 각 이름의 길이가 nameLength 리스트에 추가
// 4. 생성자 참조
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
list.add("Lennon");
list.add("Theo");
list.add("Harvey");
System.out.println(list);
- 🚀Supplier<List<String>> 의 구조
➡️Supply "공급하다" 이므로, Supplier는 “결과만을 공급해주는 것”으로 이해
(=Supplier의 특징으로 매개변수 없이 결과값만 반환하기때문) - Supplier<T>
➡️T : 제네릭 타입으로 반환하고자하는 데이터 타입을 의미하는데 get()메소드 호출 시 그 데이터 타입이 반환되는 것<T>에 < List<String>>가 위치하므로 List<String> 데이터를 반환하는 것 - ArrayList::new;
➡️필요할 때마다 새로운 ArrayList를 공급 - Supplier<List<String>> listSupplier = ArrayList::new;
➡️ArrayList::new : ArrayList 클래스의 생성자를 참조
이 부분은 () -> new ArrayList<>(); 와 같은 기능
즉, get() 메소드 호출 시 new ArrayList<>()가 실행 - List<String> list = listSupplier.get();
➡️listSupplier.get() 호출 시 새로운 ArrayList 객체가 생성되고
이 객체를 list 변수에 저장
스트림 Stream API
- 자바 8부터 다량의 데이터처리를 위해 도입됨
- 데이터의 흐름을 추상화한 객체 (메소드가 아님)
- java.util.stream 패키지에 정의된 클래스로 Stream<T> 타입으로 표현됨
- 배열, 컬렉션의 요소들을 함수형 스타일로 처리할 수 있게 도와줌
- 데이터 자체를 저장하진 않고 데이터를 처리하는 통로 역할을 함
- 데이터를 필터링, 변환하는 작업이 단순화됨
- 함수형 프로그래밍 스타일을 지원함
(자바는 명령형 프로그래밍 언어지만 스트림을 통해
함수형 프로그래밍을 도입하여 더 유연한 프로그래밍 방식이 가능하도록함) - 병렬 처리 용이성 : 멀티 코어 프로세서의 장점으로 활용하여 데이터 처리 성능 향상 가능
➡️parallelStream() 메소드를 통해 병렬처리를 간단하게 수행 가능
- 중요한 특징으로 기존 데이터는 변경하지 않고 새로운 스트림을 생성할 수 있다는 것
➡️데이터의 소스가 컬렉션, 배열, 숫자, 파일 등으로 달라도 스트림을 생성해주면
이전의 데이터 형태가 무엇이든 같은 방식으로 데이터를 처리할 수 있게되어
“코드의 재사용성이 높아진다”는 장점
스트림의 3가지 과정 (스트림 생성 → 중간연산 → 최종연산)
- 스트림 생성 : 데이터소스로부터 스트림을 생성
- 중간 연산 : 원하는 형태로 데이터를 가공
- 최종 연산 : 원하는 형태로 반환
스트림 생성
Stream<String> stream = Stream.of("a", "b", "c");
스트림 중간 연산
- 데이터 변환 혹은 필터링을 담당
- 메소드 체이닝, Method Chaining 이라고도 부름
➡️스트림을 다른 스트림으로 변환하는 연산으로 여러 개의 중간 연산이 연결될 수 있음
➡️여러 중간 연산을 연결하여 복잡한 데이터 처리 파이프라인을 구성할 수 있음 - 지연 실행(lazy Evaluation) :
➡️중간 연산은 최종 연산이 호출될때까지 실제로 실행되지 않아 데이터 처리의 효율성을 높임 - 상태 없음 or 상태 유지 :
➡️상태를 유지하지 않는 다른 메소드와 달리 sorted()와 distinct()메소드처럼 상태를 유지하기도함
- filter() : 조건에 맞는 데이터를 필터링 (=조건을 만족하는 데이터만 남기고 나머지는 제외함)
- map() : 데이터를 변환 (원하는 필드만 뽑아내거나 특정 형태로 변환) (ex. String → Name타입 등)
- sorted() : 정렬 (기본 정렬기준으로 정렬 (가나다순))
- distinct() : 중복 제거
스트림 최종 연산
- 결과를 반환
- 스트림의 요소를 소모하여 결과를 도출하거나 부작용을 발생시킴
- 최종연산이 호출되면 스트림 파이프라인이 실행되어 최종연산 이후에는 이 스트림은 사용이 불가능
- 최종연산은 즉 중간 연산들이 처리된 데이터에 적용되는 결과를 반환하는 것
- forEach() : 각 요소 출력 (스트림의 데이터를 소모하여 출력하는 용도로 사용)
- collect() : 결과를 수집하여 (리스트, 맵 등)으로 변환 (=스트림의 요소를 수집해 원하는 형태로 변환)
➡️ex. .collect(Collectors.toList()) : 데이터를 수집해 리스트 형태로 변환 - reduce() : 누적 합산 등의 집계 작업을 담당 (스트림의 데이터를 줄여 나가면서 연산을 수행하고 최종 결과를 반환)
- count() : 요소의 개수 세기
▶️실습 - 스트림의 최종 연산 예시
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.reduce(0, Integer::sum);
System.out.println("짝수의 합: " + sum);
- Arrays클래스의 asList()로 빠르게 리스트를 생성해내고
- stream()으로 스트림 생성
- filter()로 n % 2 == 0 조건에 맞는 값 찾기
- reduce()로 초기값 0으로 시작되는 sum을 조건에 맞는 값으로 누적
▶️실습 - 스프링프레임워크에서 Stream() 사용 예시
List<User> filteredUsers = users.stream()
.filter(user -> user.isActive())
.map(UserDTO::new)
.collect(Collectors.toList());
- 스프링 프레임워크에서 Rest API 사용 시
리스트를 필터링하고, 데이터베이스 조회 후 결과를 가공, DTO 변환등에 쓰임
❓스트림과 컬렉션의 차이점
➡️둘다 데이터를 다루는 자바 인터페이스이지만
데이터 처리방식에서
컬렉션은 데이터를 저장하고 관리하는구조로 데이터 저장시 모든 요소가 메모리에 저장하고
(추가, 삭제, 검색 작업 등)을 수행
스트림은 데이터의 흐름을 나타낼 뿐 데이터를 저장하지 않고 필요에 따라서는 요소를 계산함
(=지연계산, lazy evaluation : 데이터가 필요할때만 처리되고 그 이전에는 실행되지 않음)
사용목적과 특성에서
컬렉션은 데이터의 전체적 관리와 접근에 중점
스트림은 데이터 변환과 처리에 중점
병렬 처리에서
컬렉션은 병렬처리를 직접 구현해야함
Collections.synchronizedList() 사용, ConcurrentHashMap과같은 동시성 컬렉션을 사용해야함
스트림은 내장된 병렬처리 기능이 있는데 parallelStream()으로 구현 가능
데이터 처리의 효율성 부분에서는
컬렉션은 대량의 데이터 처리 시 전체 데이터가 메모리에 올라가야하므로 메모리 사용량이 많아짐
스트림은 지연계산방식을 활용하여 필요 데이터만 처리하고 나머지는 무시할 수 있어
메모리 사용량을 효율적으로 관리 가능
다양한 데이터 소스에서 스트림 생성
- 컬렉션 스트림 생성
- 대부분의 컬렉션 클래스는 stream()메소드 제공
- List나 Set컬렉션
➡️stream() 메소드 호출로 스트림 생성 가능
ex. Stream<String> myStream = Arrays.asList(”a”, “b”, “c”).stream(); - 배열
ex. Arrays.stream(arr) 처럼 사용 가능
스트림의 정적 메소드
- Stream클래스의 of(), iterate(), generate()와 같은 정적 메소드를 제공하여 스트림을 직접 생성할 수 있음
Stream<Integer> numberStream = Stream.of(1, 2, 3);
▶️실습 - 스트림 조건 필터링 (filter())
// 1. 스트림 사용 - 축구팀명 필터링 (-ham으로 끝나는 팀)
List<String> footballTeams = Arrays.asList("Westham", "Fulham", "AstonVilla", "Newcastle");
List<String> filteredTeams = footballTeams.stream().filter(s -> s.endsWith("ham")).collect(Collectors.toList());
System.out.println(filteredTeams);
// 2. 스트림 사용 X - 축구팀명 필터링 (castle로 끝나는 팀)
List<String> filteredResult = new ArrayList<>();
for(String str : footballTeams){
if(str.endsWith("castle")){
filteredResult.add(str);
}
}
System.out.println(filteredResult);
▶️실습 - 스트림. ForEach 사용
// 1. 스트림 사용 - footballTeams를 하나씩 출력하는 경우
System.out.println("\\n스트림 사용하여 Team 출력");
footballTeams.stream().forEach(System.out::println);
// 2. 스트림 사용 X - footballTeams를 하나씩 출력하는 경우
System.out.println("\\n스트림 사용하지 않고 Team 출력");
for(String str : footballTeams){
System.out.print(str + "\\t");
}
Consumer - 입력은 받고 반환값은 없는 경우
❓이미 사용한 스트림을 다시 사용하지 못함
➡️ 데이터흐름이 추상화된 형태로 Stream을 통해 이미 데이터가 흘러나갔기때문에
사용한 데이터를 다시 쓰지 못한다.
▶️실습 - 배열에서 스트림 얻어오기
// 배열에서 스트림 얻어오기
String[] names = {"Ollie", "Conor", "Lucas", "Leighton"};
// 1번 방법. 일반 람다식 사용
Arrays.stream(names).forEach(name -> System.out.println(name));
// 2번 방법. 메소드 참조를 활용하여 더 간단히 표현
Arrays.stream(names).forEach(System.out::println);
// 3번 방법. Consumer 객체 이용
Arrays.stream(names).forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
▶️실습 - 짝수만 출력하기 (스트림 이용)
// 연습. 짝수만 얻어오기
int[] iarr = {1, 3, 4, 5, 7, 2, 9, 12, 14, 13, 11};
// 1. 스트림 없이
System.out.println("\\n스트림 없이 짝수만 얻어오기");
for(int x : iarr){
if(x % 2 == 0) System.out.print(x + "\\t");
}
// 2. 스트림 사용 (정렬(sorted())까지 포함)
System.out.println("\\n스트림 사용 짝수만 얻어오기");
Arrays.stream(iarr).sorted().filter(i -> (i % 2 == 0)).forEach(System.out::println);
🚀배운 내용이 많아서 회고 정리에 시간이 많이 소요된 것 같다.
이전 회고에서의 내용을 이어서 Spring JDBC와 Spring Data JDBC를 비교해봄으로써 비슷한 용어이지만 다른 기능을 하는 것을 알 수 있었다.
회고를 정리하면서 람다부분에서 헷갈렸던 부분과 스트림 API의 활용 부분에 있어서 이해가 되지 않았던 부분을 다시 공부해볼 수 있어서 의미있었다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"SQL 기반 페이징 기법" (0) | 2025.02.06 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"Spring Data JDBC" (1) | 2025.02.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_40일차_"Spring JDBC" (1) | 2025.02.04 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_39일차_"쿠키 / 세션" (1) | 2025.02.03 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_38일차_"스프링 포워딩/리다이렉팅" (2) | 2025.01.24 |