[자바 인 액션] 03 - 람다 표현식

11 minute read

Chapter 03. 람다 표현식

람다 표현식 ?

  • 익명 클래스처럼 이름이 없는 함수
  • 메서드를 인수로 전달 가능
  • 메서드 참조 기능

3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이다.

  • 익명 : 보통의 메서드와 달리 이름이 없으므로 익명이라 표현
  • 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간경성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

람다의 구성 요소

  • 파라미터 리스트
  • 화살표 : 파라미터 리스트와 바디를 구분
  • 바디 : 람다의 반환값에 해당하는 표현식

3.2 어디에, 어떻게 람다를 사용할까?

  • 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.

  • 함수형 인터페이스란?

    정확히 하나의 추상 메서드를 지정하는 인터페이스이다. (디폴트 메서드가 있어도 상관없다.)

  • @FunctionalInterface 는 함수형 인터페이스임을 가르키는 어노테이션이다.

    → 어노테이션을 붙이면 함수형 인테페이스가 아닌 경우 즉, 메서드가 2개 이상일 경우 컴파일 단계에서 에러를 발생시킨다.

  • 함수형 인터페이스로 무엇을 할 수 있을까?

    람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급(기술적으로 함수형 인터페이스를 concreate 구현한 클래스의 인스턴스) 할 수 있다.

public interface Predicate<T> {
    boolean test (T t);
}


public interface Comparator<T> {
    int compare(T o1, T o2);
}

3.2.2 함수 디스크립터

  • 함수형 인터페이스는 오직 하나의 추상 메서드만을 정의하는 인터페이스다.
  • 함수형 인터페이스의 추상 메서드 는 람다 표현식의 시그니처를 묘사한다.
  • 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.

ex) Runnable 인터페이스 → 추상메서드 void run() : 인수와 반환값이 없는 시그니처

ex) () → void, (Apple, Apple) → int

왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있는걸까?

→ 15장, 16장, 20장, 21장에서 다시 확인

자바에 함수 형식을 추가하는 방법도 대안으로 고려했지만, 언어를 더 복잡하게 만들지 않는 현재 방법을 선택했다.

  • 함수 형식? 람다 표현식을 표현하는 데 사용한 시그니처와 같은 특별한 표기법

3.3 람다 활용 : 실행 어라운드 패턴

람다, 동작 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제

ex) 자원 처리(데이터베이스의 파일처리)에 사용하는 순환 패턴 : 자원 열기 → 처리 → 자원 닫기

실행 어라운드 패턴 : 실제 자원을 처리하는 코드설정정리 두 과정이 둘러싸는 형태

실행 어라운드 패턴을 적용하는 네 단계 과정

  1. 동작 파라미터화를 기억하라
  2. 함수형 인터페이스를 이용해서 동작 전달
  3. 동작 실행
  4. 람다 전달

함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader → StringIOException 을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader br) throws IOException;
}

BufferedReaderProcessor 에 정의된 process 메서드의 시그니처와 일치하는 람다를 전달할 수 있다.

전달된 코드는 함수형 인터페이스의 인스턴스 로 전달된 코드와 같은 방식으로 처리한다.

⇒ processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.

public class LambdaApplication {
    public static void main(String[] args) throws IOException {
        String oneLine = processFile((BufferedReader br) -> br.readLine()); //4. 람다 전달
        System.out.println("oneLine : " + oneLine);

        String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
        System.out.println("oneLine : " + twoLine);
    }

    public static String processFile(BufferedReaderProcessor brp) throws IOException { //2. 동작전달
        String path = LambdaApplication.class.getResource("").getPath(); //현재 클래스의 절대 경로
        try (BufferedReader br = new BufferedReader(new FileReader(path + "data.txt"))) {
            return brp.process(br); //3.동작 실행 - BufferedReader 객체 처리
        }
    }
}

3.4 함수형 인터페이스 사용

자바 API는 Comparable, Runnable, Callable 등의 다양한 함수형 인터페이스를 포함하고 있다.

자바 8에서는 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다. 따로 정의할 필요 없이 바로 사용할 수 있따.

  • Predicate, Consumer , Function 인터페이스를 알아보자

Predicate

java.util.function.Predicate 인터페이스

  • test라는 추상 메서드를 정의

  • 제네릭 형식 T의 객체를 인수로 받아 불린을 반환

Consumer

java.util.function.Consumer 인터페이스

  • accept라는 추상 메서드를 정의
  • 제네릭 형식 T 객체를 받아서 void를 반환
  • T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 사용

Function

java.util.function.Function 인터페이스

  • apply라는 추상 메서드를 정의
  • 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환
  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용

기본형 특화

  • 제네릭 파라미터에는 참조형만 사용할 수 있다.

    why?

    제네릭의 내부 구현 때문에 어쩔 수 없다. → 20장에서 확인

  • 박싱 : 기본형 → 참조형 변환
  • 언박싱 : 참조형 → 기본형 변환
  • 오토박싱 : 박싱과 언박싱이 자동으로 이루어지는 기능

단점

  • 변환 과정은 비용이 소모 된다.
  • 박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다.
  • 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.

해결

  • 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.
public interface IntPredicate {
  boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); //참(박싱 없음)

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); //거짓(박싱)

자바 8에 추가된 함수형 인터페이스

함수형 인터페이스 함수 디스크립터 기본형 특화 사용 사례
Predicate T → boolean IntPredicate, LongPredicate, DoublePredicate 불리언 표현
Consumer T → void IntConsumer 객체에서 소비
Function<T, R> T → R IntFunction ToIntFunction 객체에서 선택/ 추출
Supplier () → T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier 객체 생성
UnaryOperator T → T IntUnaryOperator  
BinaryOperator (T, T) → T IntBinaryOperator 두 값 조합
BiPredicate<L, R> (T, U) → boolean    
BiConsumer<T, U> (T, U) → void    
BiFunction<T, U, R> (T, U) → R    

3.5 형식 검사, 형식 추론, 제약

람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다. 따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.

3.5.1 형식 검사

  • 람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다.

    • 콘텍스트(context)란?

      람다가 전달될 메서드 파라미터, 람다가 할당되는 변수 등..

  • 대상 형식(target type) : 어떤 콘텍스트에서 기대되는 람다 표현식의 형식

형식 검사 확인 과정

List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
  1. 람다가 사용된 콘텍스트는 무엇인가? 우선 filter의 정의를 확인하다.
  2. 대상 형식은 Predicate이다.
  3. Predicate인터페이스의 추상 메서드는 무엇인가?
  4. Apple을 인수로 받아 boolean을 반환하는 test 메서드다
  5. 함수 디스크립터는 Appleboolean이므로 람다의 시그니처와 일치한다. 람다도 Apple을 인수로 받아 boolean을 반환하므로 코드 형식 검사가 성공적으로 완료된다.

3.5.2 같은 람다, 다른 함수형 인터페이스

하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할 수 있다.

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 캐스트하면 누구를 호출할 것인지 명확해진다.

형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접할 수 있으므로 람다 문법에서 이를 생략할 수 있다.

Comparator<Apple> c4 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //형식 추론하지 않음
Comparator<Apple> c5 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); //형식을 추론함

어느 코드가 가독성을 향상시킬 수 있는지 판단해서 사용하면 된다.

지역 변수 사용

람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수1를 활용할 수 있다. 이를 람다 캡처링이라고 부른다.

하지만 람다에서 변수를 캡처(자신의 바디에서 참조)하는데 제약이 있다.

  • 인스턴스 변수 정적 변수는 자유롭게 캡처할 수 있다.

  • 하지만 지역변수는 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

    (즉, final로 선언된 변수, 또는 final로 선언된 변수와 똑같이 사용되는 변수만 가능)

람다 캡처링의 제약 조건

지역 변수를 람다 캡처링 할 때 아래 두 가지 제약조건이 존재한다.

  1. 지역변수는 final로 선언돼있어야한다.
  2. final로 선언되지 않은 지역변수는 final처럼 동작해야한다. 즉, 값의 재할당이 일어나면 안 된다.

왜 지역 변수에 이런 제약이 필요할까? 왜 인스턴스 변수에는 이런 제약조건이 없는 걸까?

인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

우선 JVM의 메모리 구조를 알아야한다. JVM에서 지역 변수는 스택이라는 영역에 생성된다. 그리고 실제 메모리와는 달리 JVM에서 스택 영역은 쓰레드마다 별도의 스택이 생성된다. ⇒ 따라서 지역 변수는 쓰레드끼리 공유가 안 된다. JVM에서 인스턴스 변수는 힙 영역에 생성된다. ⇒ 인스턴스 변수는 쓰레드끼리 공유가 가능하다.

람다는 별도의 쓰레드에서 실행이 가능하다. 따라서 원래 지역 변수가 있는 쓰레드는 사라져서 해당 지역변수가 사라졌는데도 불구하고, 람다가 실행 중인 쓰레드는 살아있을 가능성이 있다.

하지만 이 람다에서 사라진 쓰레드의 지역변수를 참조하고 있으면 어떻게 될까? 당연히 오류가 날 것이다. 하지만 우리의 예상과는 달리 오류는 나지 않는다. 또한 별도의 쓰레드에서 실행된다면 별도의 스택 영역을 가질테고, 그럼 다른 쓰레드의 스택에 있는 지역변수는 참조조차 할 수 없다.

왜 오류는 나지 않고, 어떻게 다른 쓰레드의 스택 영역에 있는 지역 변수를 참조할 수 있는 걸까? 이는 람다에서 지역 변수(해당 쓰레드의 스택)에 직접적으로 접근하는 게 아니라 변수를 자신(쓰레드)의 스택에 복사하기 때문이다. 그렇기 때문에 별도의 쓰레드의 스택에 있는 지역 변수와 동일한 값을 참조할 수 있는 거고, 원래 쓰레드가 사라져도 본인의 쓰레드에서 자신의 할 일을 착실히 수행할 수 있는 것이다.

하지만 위와 같이 변수를 복사해서 쓰는데 그 변수의 값이 중구난방으로 변경된다고 하면 해당 복사본을 믿고 쓸 수 있을까? 따라서 지역 변수에는 final이어야하거나 final 같이 동작해야한다는 제약 조건이 생긴 것이다.

그렇다면 인스턴스 변수는 왜 이런 조건이 없는 걸까? 이는 인스턴스 변수는 힙에 존재하고, 쓰레드끼리 공유도 가능하기 때문에 별도로 복사할 필요도 없고, 직접 힙에 접근해서 사용하면 되기 때문이다.

람다식은 어느 메모리에 할당되는 것일까?

람다식이 결국에는 함수형 인터페이스의 인스턴스로 취급할 수 있다는 의미는 실제로 동작할 때도 함수형 인터페이스의 구현체라는 의미인가? 그렇다면 코드는 메소드 영역에 로드되고 힙 영역에서 해당 메소드의 코드 부분의 주소를 가리키고 있는 것인가?

람다식마다 코드영역에 로딩되는 것이 아니라 자바가 제공해주는 함수형 인터페이스나 직접 작성한 인터페이스 코드가 메소드 영역에 저장되고, 람다식 호출 시에는 매개변수만 가지고 이 코드를 참조하는 것인가?

3.6 메서드 참조

메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축약형이다. 메서드 레퍼런스를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주 할 수 있다.

“이 메서드를 직접 호출해”라고 명령 한다면 메서드를 어떻게 호출해야 하는지 설명 을 참조하기보다는 메서드명을 직접 참조 하는 것이 편리하다.

(* 실제 메서드를 호출하는 것은 아니므로 괄호는 필요없다.)

람다 메서드 레퍼런스 단축 표현
(Apple a) → a.getWeight() Apple::getWeight
() → Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) ⇒ str.substring(i) String::substring
(String s) → System.out.println(s) System.out::println

메서드 참조를 만드는 방법

  1. 정적 메서드 참조

    ex) Integer::parseInt

  2. 다양한 형식의 인스턴스 메서드 참조

    ex) String::length

    ⇒ 메서드 참조를 이용해서 람다 표현식의 파라미터로 전달할 수 있다.

    ​ ex) (String s) → s.toUpperCase()String::toUpperCase

  3. 기존 객체의 인스턴스 메서드 참조

    ⇒ 람다 표현식에서 현존하는 외부 객체의 메서드를 호출할 때 사용

    Transaction expensiveTransaction = new Transaction();
    // 람다 표현식
    () -> expensiveTransaction.getValue()
    // 메서드 참조
    expensiveTransaction::getValue
    

    비공개 헬퍼 메서드를 정의한 상황에서 유용하게 활용할 수 있다.

// 1.(args) -> ClassName.staticMethod(args)ClassName::staticMethod  // 2.(arg0, rest) -> arg0.instanceMethod(rest) //arg0은 ClassName 형식ClassName::instanceMethod//3.(args) -> expr.instanceMethod(args)expr::instanceMethod

2번 예제

List<String> str = Arrays.asList("a", "b", "A", "B");str.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); //람다str.sort(String.compareToIgnoreCase); //메서드 참조

비공개 헬퍼 메서드

Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);Predicate<String> startsWithNumber = this.startsWithNumber

3.6.2 생성자 참조

package dev.solar.chapter03;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.function.BiFunction;import java.util.function.Function;import java.util.function.Supplier;public class ConstructorReference {    public static void main(String[] args) {        Apple apple;        // 1. 기본 생성자 참조        Supplier<Apple> c1;//        c1 = () -> new Apple();        c1 = Apple::new;        apple = c1.get();        System.out.println("Supplier Apple : " + apple);        // 2. 하나의 매개변수 생성자 참조        Function<Integer, Apple> c2;//        c2 = (weight) -> new Apple(weight);        c2 = Apple::new; //Apple(Integer weight) 생성자 참조        apple = c2.apply(110);        System.out.println("Function Apple : " + apple);        List<Integer> weights = Arrays.asList(7, 3, 4, 10);        List<Apple> apples = map(weights, Apple::new);        apples.forEach(System.out::println);        // 3. 두 개의 매개변수 생성자 참조        BiFunction<String, Integer, Apple> c3;//        c3 = (color, weight) -> new Apple(color, weight);        c3 = Apple::new;//Apple(String color, Integer weight) 생성자 참조        apple = c3.apply("red", 200);        System.out.println("BiFunction Apple : " + apple);        // 4. 3 개의 매개변수 생성자 참조 -> 직접 구현해야 한다.        TriFunction<String, Integer, String, Apple> c4;//        c4 = (color, weight, area) -> new Apple(color, weight, area);        c4 = Apple::new; //Apple(final String color, final Integer weight, final String area) 생성자 참조        apple = c4.apply("GREEN", 140, "대구");        System.out.println("TriFunction Apple : " + apple);    }    public static List<Apple> map(List<Integer> weights, Function<Integer, Apple> f) {        List<Apple> result = new ArrayList<>();        for (Integer weight : weights) {            result.add(f.apply(weight));        }        return result;    }    public interface TriFunction<T, U, V, R> {        R apply(T t, U u, V v);    }}
Supplier Apple : Apple{color='null', weight=null, area='null'}Function Apple : Apple{color='null', weight=110, area='null'}Apple{color='null', weight=7, area='null'}Apple{color='null', weight=3, area='null'}Apple{color='null', weight=4, area='null'}Apple{color='null', weight=10, area='null'}BiFunction Apple : Apple{color='red', weight=200, area='null'}TriFunction Apple : Apple{color='GREEN', weight=140, area='대구'}

3.7 람다, 메서드 참조 활용하기

코드전달 → 익명 클래스 사용 → 람다 표현식 사용 → 메서드 레퍼런스 사용

3단계 : 람다 표현식

  • Comparator의 함수 디스크립터 (T, T) → int 이다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight())); //형식 추론

ComparatorComparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 comparing을 포함한다.

람다 표현식은 사과를 비교하는 데 사용할 키를 어떻게 추출할 것인지 지정하는 한 개의 인수만 포함한다.

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

그러므로 다음처럼 사용할 수 있다.

import static java.util.Comparator.comparing;inventory.sort(comparing((a) -> a.getWeight()));

→ comparing 메서드가 정적 메서드인 이유는 9장에서 설명

4단계 : 메서드 레퍼런스

import static java.util.Comparator.comparing; //정적으로 임포트했다고 가정inventory.sort(comparing(Apple::getWeight));

3.8 람다 표현식을 조합할 수 있는 유용한 메서드

간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다.

ex) 두 프레디케이트의 or 연산을 수행하는 커다란 프레디케이트, 한 함수의 결과가 다른 함수의 입력이 되도록 두 함수를 조합

3.8.1 Comparator 조합

역정렬

비교자 구현을 그대로 재사용하여 사과의 무게를 기준으로 역정렬할 수 있다.

// 무게를 내림차순으로 정렬Inventory.sort(comparing(Apple::getWeight).reserved()); 

Comparator 연결

비교 결과를 더 다듬을 수 있는 두번째 Comparator를 만들 수 있다.

ex) 두 사과를 비교해서 무게가 같다면 원산지 국가별로 사과를 정렬할 수 있다.

thenComparing 메서드로 두 번째 비교자를 만들 수 있다.

함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.

inventory.sort(comparing(Apple::getWeight)    .reversed() 												//무게를 내림차순으로 정렬하고    .thenComparing(Apple::getContry));			//두 사과의 무게가 같으면 국가별로 정렬

3.8.2 Predicate 조합

Predicate 인터페이스는 복잡한 Predicate를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.

// 빨간색이면서 무거운(150그램 이상) 사과 또는 그냥 녹색 사과Predicate<Apple> redAndHeavyAppleOrGreen =                                 redApple  															.and(a -> a.getWeight() > 150)                                    	.or(a -> "green".equals(a.getColor()));

3.8.3 Function 조합

Function 인터페이스는 andThen, compose 두 가지 디폴트 메서드를 제공한다.

andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.

// h 함수 : 숫자를 +1 증가시키고, 그 결과값에 *2Function<Integer, Integer> f = x -> x + 1;Function<Integer, Integer> g = x -> x * 2;Function<Integer, Integer> h = f.andThen(g); //수학적 표현 : g(f(x))int result = h.apply(1); // 4

compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.

// h 함수 : 숫자 * 2를 한 값에 + 1Function<Integer, Integer> f = x -> x + 1;Function<Integer, Integer> g = x -> x * 2;Function<Integer, Integer> h = f.compose(g); //수학적 표현 : f(g(x))int result = h.compose(1); // 3

정리

  • 람다 표현식익명 함수의 일종이다. 이름은 없지만 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
  • 람다 표현식으로 간결한 코드를 구현할 수 있다.
  • 함수형 인터페이스는 오직 하나의 추상 메서드만을 정의하는 인터페이스다.
  • 함수형 인터페이스의 추상 메서드 는 람다 표현식의 시그니처를 묘사한다.
  • 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.
  • 함수형 인터페이스를 기재하는 곳에서만 람다 표현식을 사용할 수 있다.
  • 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공할 수 있으며 람다 표현식 전체함수형 인터페이스의 인스턴스로 취급된다.
  1. 자유 변수 : 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수