728x90

 

자바에서 인터페이스를 사용하는 이유가 무엇일까?

아마 인터페이스를 처음 접해보는 분들이 많이 궁금해 하는 부분일거라고 생각한다.

그냥 그 기능을 클래스로 구현해도 될 것 같은데 번거롭게(?) 인터페이스까지 사용해서 또 인터페이스에 대한 학습도 해야하기 때문이다.

 

이세상 모든건 그게 왜 필요한지를 알면 이해하는데 도움이 많이 된다.

 

interface라고 쓰는 이 인터페이스는 결론부터 얘기하면 '공동 작업시 충돌을 방지하기 위해서'라고 한다.

다른 많은 이점이 있지만, 처음 내가 배울 당시 인터페이스 사용 이유는 공동 작업을 할때 유연함을 위해서다.

 

다음과 같은 상황을 생각해보자.

A씨는 필기도구를 사용해서 글씨를 쓰는 프로그램을 개발하는 프로젝트를 진행하고 있다.

그러던 중 같은 팀의 연필을 개발하는 김개발씨와 볼펜을 개발하는 박개발씨에게 '쓴다'라는 기능을 개발하라고 지시했다.

 

김개발씨의 코드는 다음과 같다.

 

class Pencil {
    public void Write() {
        System.out.println("연필로 쓴다.");
    }
}

 

박개발씨의 코드는 다음과 같다.

 

class Ballpoint {
    public void Writing() {
        System.out.println("볼펜으로 쓴다.");
    }
}

 

두 사람에게 개발을 완료했다는 말을 들은 A씨는 당황했다.

각자의 단위에서는 잘 실행이 되지만, 메인 프로그램에 붙여서 사용하자니 서로 이름이 너무 달라서 사용하기에 너무 불편한 것이다.

왜냐면 다음주에 형광펜, 네임펜 등의 필기구에 대해서도 개발을 해야하기 때문에 이대로는 안되겠다 싶었다.

 

class MainClass {
    public static void main(String[] ar) {
        Pencil pencil = new Pencil();
        BallPoint ballpoint = new Ballpoint();

        pencil.write();
        ballpoint.writing();
    }
}

 

 

728x90

 

 

그래서 미안하지만 김개발, 박개발씨에게 다음과 같은 인터페이스를 주며 다시 한 번 개발해달라고 부탁했다.

 

interface Frindle {
    public void Write();
}

 

그리고나서 개선된 김개발씨의 코드이다.

 

class Pencil implements Frindle {
    @Override
    public void Write() {
        System.out.println("연필로 쓴다.");
    }
}

 

박개발씨의 코드는 다음과 같다.

 

class Ballpoint implements Frindle {
    @Override
    public void Write() {
        System.out.println("볼로 쓴다.");
    }
}

 

이로써 A씨는 다음과 같은 메인 클래스를 작성할 수 있게 되었다.

 

package com.tistory.xxxelppa;

public class MainClass {
    public static void main(String[] ar) {
        Frindle frindle = new Pencil();
        frindle.write();
        
        frindle = new Ballpoint();
        frindle.write();
        
    }
}

 

이제는 형광펜으로 써도, 네임펜으로 써도 A씨는 클래스 이름만 알면 된다.

그러면 write라는 메서드를 호출했을 때 어떤 결과를 받아볼 것이라는 기대를 할 수 있게 된다는 장점이 있다.

 

(더 좋고 유연하게 작성할 수 있겠지만, 지금 당장 생각나는 예제가 이것 뿐이다.)

 

 

만약 형광펜으로 쓴다는 기능을 추가한다고 할 때, 이 interface를 상속받아 사용하게 된다면

A씨는 형광펜 클래스 내부에서 어떤 작업을 하는지 모르겠지만, write()를 호출했을 때 형광펜으로 쓴다는 기능을 한다는 것을 기대할 수 있다는 것이다.

 

다른 여러 장점을 가지고 있지만(인터페이스가), 내가 처음 배울 당시 인터페이스를 사용해야 하는 이유로 공동작업의 예를 들었기 때문에

위와같은 예시를 들어 보았다.

 

 

 

 

728x90
728x90

 

1편에서 람다식에서 생략 가능한 부분이 있고, 함수형 인터페이스를 사용하기 때문에 발생하는 람다식 안에서 사용하는 변수의 제한적인 부분이 있다고 했었다.

 

 

우선 람다식에서 생략 가능한 문법에 대해서 정리 해보자.

 

추상메서드의 매개변수에 따라 생략 가능한 부분이 조금 다르기 때문에, parameter가 있는 경우와 없는 경우로 나누어 살펴보자.

 

 

1. parameter : 없음

 

public class FunctionalInterfaceExample {
    public static void main(String[] ar) {
        // 매개변수가 없는 경우 ()는 무조건 작성해 줘야 한다.
        FInterface fi_1 = () -> { };
  
        // 구현부가 여러 라인인 경우 { } 안에 작성해야 한다.
        FInterface fi_2 = () -> {
            String msg = "Hello";
            System.out.println(msg);
        };

        // 구현부가 1라인인 경우 { }를 생략할 수 있다.
        FInterface fi_3 = () -> System.out.println("Hello");

        fi_1.aaa();
        fi_2.aaa();
        fi_3.aaa();
    }
}

interface FInterface {
    public void aaa();
}

 

 

 

2. parameter : 있음

 

public class FunctionalInterfaceExample {
    public static void main(String[] ar) {
        // 기본형
        FInterface fi_1 = (x) -> { };
  
        // 매개변수가 1개인 경우 ()를 생략할 수 있다.
        FInterface fi_2 = x -> { };
  
        // 구현부가 여러 라인인 경우 { } 안에 작성해야 한다.
        FInterface fi_3 = (x) -> {
            String msg = "Hello";
            System.out.println(msg + " : " + x);
        };
  
        // 구현부가 1라인인 경우 { }를 생략할 수 있다.
        FInterface fi_4 = (x) -> System.out.println("Hello" + " : " + x);

        // 매개변수가 1개인 경우 ()를 생략할 수 있고, 구현부가 1라인인 경우 { }를 생략할 수 있다.
        FInterface fi_5 = x -> System.out.println("Hello" + " : " + x);
  
        fi_1.aaa(10);
        fi_2.aaa(20);
        fi_3.aaa(30);
        fi_4.aaa(40);
        fi_5.aaa(50);
    }
}

interface FInterface {
    public void aaa(int x);
}

 

 

728x90

 

 

마지막으로 람다식 구현부 안에서 사용한 변수는 수정할 수 없다는 제약이 존재한다.

즉, final 변수로 취급된다.

 

public class FunctionalInterfaceExample {
    public static void main(String[] ar) {

        int tmp = 10;
        tmp = 200;
  
        FInterface fi = (x) -> {
            System.out.println("x : " + x);
        };
    }
}

interface FInterface {
    public void aaa(int x);
}

 

 

위의 경우 람다식 안에서 아직 tmp 변수를 사용하고 있지 않기 때문에 아무런 문제가 없다.

하지만 다음과 같은 경우 문제가 된다.

 

public class FunctionalInterfaceExample {
    public static void main(String[] ar) {

        int tmp = 10;
        tmp = 200;
        FInterface fi = (x) -> {
            System.out.println("x : " + x);
            // Local variable tmp defined in an enclosing scope must be final or effectively final
            System.out.println("tmp : " + tmp);
        };
    }
}

interface FInterface {
    public void aaa(int x);
}

 

 

이렇게 람다식 안에서 사용할 변수들에 대해서는 final 변수 취급이 되기 때문에 5라인에서처럼 값을 수정할 수 없다.

8라인은 5라인에서의 오류 메시지를 보여준다.

 

 

public class FunctionalInterfaceExample {
    public static void main(String[] ar) {

        final int tmp = 10;
        // tmp = 200;
        FInterface fi = (x) -> {
            System.out.println("x : " + x);
            // Local variable tmp defined in an enclosing scope must be final or effectively final
            System.out.println("tmp : " + tmp);
        };
    }
}

interface FInterface {
    public void aaa(int x);
}

 

 

그렇기 때문에 람다식 안에서 사용 할 변수들에 대해서는 위의 4라인과 같이 final 을 명시적으로 작성해주는 것이 가독성에 더 좋다.

이렇게 람다식 안에서 사용하는 변수가 final 취급받는 이유는,

이 람다식이 함수형 인터페이스이기 때문이다.

인터페이스에서 선언된 멤버필드는 기본적으로 생략하더라도 무조건 static final형태이기 때문이다.

그래서 람다식도 결국 익명 함수를 구현하는 함수형 인터페이스이기 때문에 사용하는 변수도 final의 성격을 가질 수 밖에 없다.

 

 

 

 

728x90
728x90

 

Java 8 에서 새롭게 선보인 람다식.

 

이 람다식에 대해 알아보기 전에 꼭 알아야 할 개념이 있다.

 

 

 

futional interface라는 것이다.

이 함수형 인터페이스란 이름에 겁먹지 말자.

 

함수형 인터페이스란 추상메소드를 단 하나만 가지는 인터페이스를 지칭하는 말이다.

당장 생각나는 인터페이스중에는 Runnable 인터페이스가 있지만, 임의로 함수형 인터페이스를 하나 생성해서 예시로 들어보겠다.

 

interface FInterface {
    public void aaa();
} 

 

 

위에서 처럼 FInterface라는 이름의 인터페이스를 하나 생성했다.

그리고 이 인터페이스는 aaa라는 이름의 매개변수가 없고 리턴타입이 void인 추상메소드를 하나 가지고 있다.

이게 함수형 인터페이스이다.

 

 

 

 

추가로 인터페이스가 함수형 인터페이스임을 명시적으로 선언할 수 있다.
바로 @ (어노테이션)을 사용하는 방법이다.

이 어노테이션은 @FuntionalInterface 라고 쓴다.

 

@FunctionalInterface
interface FInterface {
    public void aaa();
}

 

위에서와 같이 명시적으로 이 인터페이스가 함수형 인터페이스임을 명시했다면,
이 인터페이스가 두 개 이상의 추상 메서드를 가질 경우 컴파일 시기에 에러를 보여준다.

즉, 다음과 같이 사용할 수 없다.

 

@FunctionalInterface
interface FInterface {
    public void aaa();
    public void bbb();
} 

 

이렇게 작성할 경우 1번 라인에
Invalid '@FunctionalInterface' annotation; FInterface is not a functional interface
라는 오류를 발생한다.


** @FunctionalInterface를 명시할 경우 두 개 이상의 추상메서드를 가질 수 없도록 컴파일 시기에 에러를 발생할 뿐이다.
굳이 명시하지 않더라도 추상메서드가 하나일 경우 함수형 인터페이스로 인식하는데는 문제가 없다.

 

728x90

 

 

 

람다식에 대해서 말하고 있는데 왜 갑자기 함수형 인터페이스에 대해서 언급하는 것일까?

당연히 밀접한 관련이 있기 때문이다.

람다식이란 다른 말로 익명함수라고도 부른다.

익명함수란 이름이 없는 함수라는 말이다.

 

사용함에서의 장점은 코드가 간결해지고 뷸팔요한 반복이 제거되며 동일 함수에 대한 재사용성이 좋아진다.

이렇게 필요한 부분에 대해서만 작성하기 때문에 더 나은 퍼포먼스를 기대할 수도 있다.

 

 

더 나은 퍼포먼스에 대해서는 객체를 만드는 과정을 생략하기 때문이다.

 

Runnable r1 = new Runnable() {
    @Override
    public void run() {
    
    }
};

Runnable r2 = () -> { };

 

위에서 선언한 r1와 r2는 완전히 동일한 것으로 간주할 수 있다.
1라인에서는 new 라는 연산자를 통해 객체를 생성하고 있다.
반면 8라인에서 람다식을 사용할 경우 객체 생성없이 함수를 호출하듯 바로 사용이 가능하다.
이제부터 람다식의 더 자세한 사용 방법에 대해 알아보자.

 

 

 

 

람다식은 자바 진영 에서도 이제부터 함수형 언어를 지원하겠다는 것을 의미한다.

함수는 보퉁 y = f(x) 라고 표현한다.

이것의 의미는 f라는 이름의 함수에 x값을 넣으면 y값을 결과로 받아볼 수 있음을 뜻한다.

 

즉, 람다식도 동일하다.

( ) -> { } 이렇게 표현하는 람다식은

( ) 이 안에 매개변수를 넣고 { } 안의 로직을 수행한다.

 

매개변수가 x이고 이 x값을 화면에 출력하는 람다식을 작성해보자.

 

public class FuntionalInterfaceExample {
    public static void main(String[] ar) {
        FInterface fi = (x) -> {
            System.out.println(x);
        };
        fi.aaa(10);
    }
}

interface FInterface {
    public void aaa(int x);
}

 

위 소스의 실행 결과는 콘솔창에 10이라는 값을 출력하는 것이다.

3라인의 (x)가 { ... } 에서 사용할 매개변수를 뜻하며, { ... }안에서 x변수를 출력하는 예제이다.

 

이번엔 두 개의 매개변수를 받아 출력하는 예제를 작성해보자.

 

public class FuntionalInterfaceExample {
    public static void main(String[] ar) {
        FInterface fi = (x, y) -> {
            System.out.println(x+y);
        };
        fi.aaa(10, 20);
    }
}

interface FInterface {
    public void aaa(int x, int y);
}

 

위 소스의 실행 결과는 콘솔팡에 30이라는 값을 출력하는 것이다.

만약 람다식이 없었다면 어떻게 작성했을지 비교를 위해 람다식을 사용하지 않은 소스 코드를 첨부한다.

 

public class FuntionalInterfaceExample {
    public static void main(String[] ar) {
        /*
        FInterface fi = (x) -> {
            System.out.println(x);
        };
        fi.aaa(10);
        */
        FInterface fi2 = new FInterface() {
            @Override
            public void aaa(int x, int y) {
                System.out.println(x+y);
            }
        };
        fi2.aaa(10, 20);
    }
}

interface FInterface {
    public void aaa(int x, int y);
}

 

 

그렇다면 이 람다식은 기존의 방식처럼 new 연산자를 사용해서 어떤 인터페이스를 사용할지 명시해주지도 않았는데 어떤 인터페이스를 사용할지 결정했을까?

 

람다식은 내가 담길 변수의 타입에 해당하는 인터페이스를 사용하도록 만들어졌다.

즉, 내가 FInterface fi 라고 하는 순간 FInterface의 추상메서드를 알아서 호출하도록 되어 있기 때문에 해당 인터페이스의 매개변수와 리턴타입만 맞춰주면 나머지는 알아서 처리한다.

 

 

마지막으로 이 람다식에서 생략 가능한 부분이 있고 함수형 인터페이스를 사용하기 때문에 발생하는 람다식 안에서 사용하는 변수의 제한적인 부분에 대해서는 다음에 살펴보겠다.

 

 

 

 

728x90

+ Recent posts