1. 글씨가 전부다 같은 경우 : -1
  2. 글씨가 전부다 다른 경우 : 글자 수
  3. 팰린드롬인 경우 : 글자수 -1 3가지 경우의 수로 나눠서 코드 짜면 됩니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;

public class boj15927 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str = br.readLine();
        char[] arr = str.toCharArray();
        HashSet<Character> hs = new HashSet<>();
        for (int i = 0; i < arr.length / 2; i++) {
            hs.add(arr[i]);
            if (arr[i] != arr[arr.length - 1 - i]) {
                System.out.println(arr.length);
                return;
            }
        }
        for (int i = arr.length / 2; i < arr.length; i++) {
            hs.add(arr[i]);
        }
        if(hs.size()==1){
            System.out.println(-1);
        }else{
            System.out.println(arr.length-1);
        }

    }
}

'Algorithm > BOJ' 카테고리의 다른 글

백준 1546번. 평균 (Java)  (0) 2022.08.12
백준 11720. 숫자의 합 (Java)  (0) 2022.08.12
[백준 2493] 탑 (Java)  (0) 2022.01.05
[백준 10211] Maximum Subarray (Java)  (0) 2022.01.05
[백준 1759] 암호 만들기 (Java)  (0) 2022.01.02

목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식이란?

메소드를 하나의 간결한 식(expression)으로 표현한 것

메소드를 람다식으로 표현하면 메소드의 이름과 반환값이 없어지므로 람다식을 '익명 함수(anonymous function)'라고도 한다.

 

람다식은 FunctionalInterface의 조건을 충족 해야 사용이 가능

FunctionalInterface : 오직 하나의 메소드 선언을 갖는 인터페이스

 

※메소드와 함수

메소드와 함수는 같은 의미지만 용어를 분리해서 사용했다. Java의 메소드는 객체의 행위,동작을 의미하며 특정 클래스에 반드시 속해야 한다는 제약이 있다. 그러나 JDK 1.8이후 람다식의 도입으로 메소드가 하나의 독립적인 기능을 하기 때문에 익명 함수라는 용어를 사용한다.

 

람다식 사용법

 

메소드의 이름과 반환타입을 제거하고 매개변수 선언부와 body{  } 사이에 ->를 추가한다.

ReturnType methodName(Parameter p) { 
	// body
}

(Parameter p) -> {
	// body
}

두 값 중에 큰 값을 반환하는 메소드 max()를 람다식으로 변환하기

int max(int a, int b) {
	return a > b ? a : b;
}

(int a, int b) -> {
	return a > b ? a : b;
}

return문 대신 식(expression)으로 대신할 수 있다. 식의 연산 결과가 자동으로 반환값이 된다.

문장이 아닌 식으로 끝에 세미콜론( ; )을 붙이지 않는다.

(int a, int b) -> a > b ? a : b

매개변수의 타입은 추론이 가능한 경우(대부분의 경우) 생략 가능하다.

참고로 반환타입을 제거할 수 있는 이유도 항상 추론이 가능하기 때문이다.

(a, b) -> a > b ? a : b

 

 

람다식 작성 문법 정리

 

1. 기본적인 작성 규칙
- 이름과 반환타입은 작성하지 않는다. (anonymous function)

2. 매개변수
- 추론이 가능한 매개변수의 타입은 생략할 수 있다.
- 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
- 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
- 단, 매개변수의 타입을 작성한 경우엔 매개변수가 하나라도 괄호( )를 생략할 수 없다.

3. body { }
- return문(return statement) 대신 식(expression)으로 대체할 수 있다.
- 식(expression)의 끝에 세미콜론(;)은 붙이지 않는다.
- 괄호{ } 안의 문장이 하나일 때는 괄호{ }를 생략할 수 있다.
- 이 때, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
- 그러나 return문은 괄호를 생략할 수 없다.

 

매개변수 규칙
  • 추론이 가능한 매개변수의 타입은 생략할 수 있다.
  • 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b

 

  • 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
  • 단, 매개변수의 타입이 있으면 괄호( )를 생략할 수 없다.
a -> a * a	// OK

int a -> a * a	// Error

body { } 규칙
  • return문 대신 식으로 대체 할 수 있다.
  • 단, 식의 끝에 세미콜론(;)은 붙이지 않는다.
(int a, int b) -> {
	return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b

 

  • 괄호 { } 안의 문장이 하나일 때 괄호 { }를 생략 할 수 있다.
  • 단, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
(String name, int i) -> { System.out.println(name + "=" + i); }
(String name, int i) -> System.out.println(name + "=" + i)

함수형 인터페이스 (Functional Interface)

 

함수형 인터페이스는 람다식을 다루는 인터페이스이다. @FunctionalInterface 어노테이션을 사용한다.

 

람다식은 실제로 메소드 그 자체가 아닌 익명 클래스의 객체와 동등하다.

 

익명 객체의 메소드와 람다식의 매개변수, 반환값이 일치하면 익명 객체를 람다식으로 대체 할 수 있다.

@FunctionalInterface
interface MyFunction {
	public abstract int max(int a, int b);
}

위 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성 할 수 있다. 

MyFunction f = new MyFunction() {	//MyFunction인터페이스를 구현한 익명 클래스의 객체 생성
	public int max(int a, int b) {
    return a > b ? a : b
    }
}
int big = f.max(5, 3);	// 익명 객체의 메소드 호출    

여기서 MyFunction 인터페이스에 정의된 메소드 max( )는 람다식 '(int a, int b) -> a > b ? a : b' 와 일치한다.람다식은 익명 객체와 동등하므로 아래와 같이 대체 할 수 있다.

MyFunction f = (a , b) -> a > b ? a : b;	// 익명 객체를 람다식으로 대체

int big = f.max(5 , 3);	// 익명 객체의 메소드 호출

이렇게 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 할 수 있는 이유는

람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메소드 max( )와

람다식의 매개변수 타입과 개수 그리고 반환값이 일치하기 때문이다.

 

하나의 메소드가 선언된 인터페이스를 정의해서 

람다식을 이용하는 것은 기존의 자바의 규칙을 어기지 않으면서 자연스럽다.

그렇기 때문에 인터페이스를 통해 람다식을 다루기로 결정되었고,

람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)라 부르기로 했다.

@FunctionalInterface
interface MyFunction {
	public abstract int max(int a, int b);
}

단, 함수형 인터페이스에서는 오직 하나의 추상 메소드만 정의되어 있어야 한다는 제약이 있다.

그래야 람다식과 인터페이스의 메소드가 1 : 1로 연결될 수 있기 때문이다.

다만, static 메소드와 default 메소드의 개수에는 제약이 없다.

함수형 인터페이스로 구현한 인터페이스라면 반드시 '@FunctionalInterface' 어노테이션을 정의하자.

왜냐하면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주기 때문이다.

 

기본 함수형 인터페이스

자바에서 기본적으로 제공하는 함수형 인터페이스는 다음과 같은 것들이 있다.

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate

이 외에도 다양한 것들이 있다.

 

Runnable

Runnable은 인자를 받지 않고 리턴값도 없는 인터페이스이다.

public interface Runnable {
	public abstract void run();
}    

아래 코드처럼 사용할 수 있다.

Runnable runnable = ( ) -> System.out.println("run anything!");
runnable.run();
// 결과
// run anything!

Runnable은 run( )을 호출해야 한다. 함수형 인터페이스마다 run( )과 같은 실행 메소드 이름이 다르다.인터페이스 종류마다 만들어진 목적이 다르고, 그 목적에 맞는 이름을 실행 메소드 이름으로 정하였기 때문이다.

 

Supplier

Supplier<T>는 인자를 받지 않고 T타입의 객체를 리턴한다.

public interface Supplier<T> {
	T get();
}    

아래 코드처럼 사용할 수 있다.

Supplier<String> getString = () -> "Good morning!";
String str = getString.get();
System.out.println(str);

// 결과
// Good morning!

 

Consumer

Consumer<T>는 T 타입의 객체를 인자로 받고 리턴 값은 없다.

public interface Consumer<T> {
	void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
    	Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

아래 코드처럼 사용할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
printString.accept("me");
// 결과
// Help me!!

또한, andThen( )을 사용하면 두개 이상의 Consumer를 연속적으로 실행할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
Consumer<String> printString2 = text -> System.out.println("--> Yes");
printString.andThen(printString2).accept("me");
// 결과
// Help me!!
// --> Yes

 

Function

Function<T, R>는 T타입의 인자를 받고, R타입의 객체를 리턴한다.

public interface Function<T, R> {
	R apply(T t);
    
    default <V> FUnction<V, R> compose(Function<? Super V, ? extends T> before) {
    	Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
        default <V> FUnction<T, V> compose(Function<? Super R, ? extends V> after) {
    	Objects.requireNonNull(after);
        return (T t) -> after.apply(after.apply(t));
    }
    
    static <T> Function<T, T> identity() {
    	return t -> t;
    }
}

다음과 같이 사용할 수 있다. apply() 메소드를 사용한다.

Function<Integer, Integer> multiply = (value) -> value *2;
Integer result = multiply.apply(3);
System.out.println(result);
// 결과
// 6

 

compose( ) 는 두개의 Function을 조합하여 새로운 Function 객체를 만들어주는 메소드이다.

주의할 점은 andThen() 과는 실행 순서가 반대이다.

compose( ) 인자로 전달되는 Function이 먼저 수행되고 그 이후에 호출하는 객체의 Function이 수행된다.

 

예를들어, 다음과 같이 compose를 사용하여 새로운 Function을 만들 수 있다. 

apply를 호출하면 add 먼저 수행되고 그 이후에 multiply가 수행된다.

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add      = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

Integer result1 = addThenMultiply.apply(3);
System.out.println(result1);
// 결과
// 12

 

Predicate

Predicate<T> 는 T타입 인자를 받고 결과로 boolean을 리턴한다.

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

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

다음과 같이 사용할 수 있다. test( ) 메소드를 사용한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
System.out.println("10 is bigger than 5? -> " + isBiggerThanFive.test(10));
// 결과
// 10 is bigger than 5? -> true

 

and( )와 or( )는 다른 Predicate와 함께 사용된다.

and( )는 두 개의 Predicate가 true일 때 true를 리턴하며, or( )는 두 개중에 하나만 true면 true를 리턴한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
Predicate<Integer> isLowerThanSix = num -> num < 6;
System.out.println(isBiggerThanFive.and(isLowerThanSix).test(10));
System.out.println(isBiggerThanFive.or(isLowerThanSix).test(10));
// 결과
// false
// true

 

isEqual( )은 static 메소드로, 인자로 전달되는 객체와 같은지 체크하는 Predicate 객체를 만들어 준다.

Predicate<String> isEquals = Predicate.isEqual("Google");
isEquals.test("Google");
// 결과
// true

Variable Capture

람다식에서 외부 지역변수를 참조하는 행위를  Lambda Capturing(람다 캡쳐링)  이라고 한다.

 

람다에서 접근가능한 변수는 아래와 같이 세가지 종류가 있다.

 

1. 지역 변수

2. static 변수

3. 인스턴스 변수

 

지역변수만 변경이 불가능하고 나머지 변수들은 읽기 및 쓰기가 가능하다.

람다는 지역 변수가 존재하는 스택에 직접 접근하지 않고, 지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다.

각각의 쓰레드마다 고유한 스택을 갖고 있기 때문에  지역 변수가 존재하는 쓰레드가 사라져도

람다는 복사된 값을 참조하면서 에러가 발생하지 않는다.

 

그런데 멀티 쓰레드 환경이라면, 여러 개의 쓰레드에서 람다식을 사용하면서 람다 캡쳐링이 계속 발생하는데

이 때 외부 변수 값의 불변성을 보장하지 못하면서 동기화 문제가 발생한다.

이러한 문제로 지역변수 final,  Effectively Final  제약조건을 갖게된다.

Effectively Final
람다식 내부에서 외부 지역변수를 참조하였을 때 지역 변수는 재할당을 하지 않아야 하는 것을 의미

 

인스턴스 변수나 static 변수는 스택 영역이 아닌 힙 영역에 위치하고,

힙 영역은 모든 쓰레드가 공유하고 있는 메모리 영역이기 때문에, 값의 쓰기가 발생하여도 별 문제가 없는 것이다.

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

class Outer {
    int val = 10;

    class Inner {
        int val = 20;

        void method(int i) {    // void method(final int i)
            int val = 30;   // final int val = 30;
//            i = 10;       // ERROR. 상수의 값은 변경할 수 없다.

            MyFunction f = () -> {
                System.out.println("             i : " + i);
                System.out.println("           val : " + val);
                System.out.println("      this.val : " + ++this.val);
                System.out.println("Outer.this.val : " + ++Outer.this.val);
            };

            f.myMethod();
        }
    } // End Of Inner
}   // End Of Outer

public class LambdaEx {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}
// 결과
             i : 100
           val : 30
      this.val : 21
Outer.this.val : 11

 

  • 람다식 내에서 참조하는 지역변수 final이 붙지 않아도 상수로 간주된다.
  • 람다식 내에서 지역변수 i와 val을 참조하고 있으므로 람다식 내에서나 다른 곳에서도 이 변수들의 값을 변경할 수 없다.
  • 반면, Inner 클래스와 Outer 클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지
  • 않아서 값을 변경해도 된다.
  • 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다.
  • 따라서 위의 코드에서 this는 중첩 객체인 Inner이다.

 


메소드, 생성자 레퍼런스

 

메소드 참조

람다식이 하나의 메소드만 호출하는 경우에는 '메소드 참조(method reference)' 를 통해 람다식을 간략하게 작성할 수 있다.

 

메소드 참조를 작성하는 방법은 아래와 같다.

클래스이름::메소드이름
or
참조변수::메소드이름
Function<String, Integer> f = s -> Integer.parseInt(s);

보통 위처럼 람다식을 작성하는 데, 이 람다식을 메소드로 표현하면 아래와 같다.

// Test Method
Integer exam(String s) {
	return Integer.parseInt(s)
}

이 wrapper 메소드는 별로 하는 일이 없다. 값을 받아서 Integer.parseInt()에게 넘겨줄 뿐이다.

 

Function<String, Integer> f = s -> Integer.parseInt(s);

// 메소드 참조
Function<String, Integer> f = Integer::parseInt;

메소드 참조를 이용하면 더 간략하게 작성할 수 있다.

위 메소드 참조에서 람다식 일부가 생략되었지만, 컴파일러는 생략된 부분을 우변의 parseInt 메소드의 선언부로부터,

또는 좌변의 Function 인터페이스에 지정된 제네릭 타입으로부터 쉽게 알아낸다.

 

또 다른 예를 보자.

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equlas(s2);

위 람다식에서 어떤 부분을 변경할 수 있을까?

 

  • 참조변수 f의 타입으로 유추하면 람다식이 두 개의 String 타입의 매개변수를 받는다. 따라서, 매개변수 생략가능.
  • 매개변수를 생락하면, equals만 남는데, 두 개의 String을 받아서 Boolean을 반환하는 equals라는 메소드 이므로
  • String::equals로 변경한다.
BiFunction<String, String, Boolean> f = String::equals;

 

메소드 참조로 변경한 결과이다.

MyClass obj = new MyClass();
Function<String, Boolean> f = x -> obj.equals(x);   //  람다식
Function<String, Boolean> f2 = obj::equals;         //  메소드 참조

메소드 참조를 사용하는 경우가 한 가지 더 있는데, 이미 생성된 객체의 메소드를 람다식에서 사용한 경우에는

클래스 이름 대신 그 객체의 참조변수 를 적어야 한다.

종류 람다 메소드 참조
static 메소드 참조 x -> ClassName.method(x) ClassName::method
인스턴스 메소드 참조 (obj, x) -> obj.method(x) ClassName:method
특정 객체 인스턴스 메소드 참조 x -> obj.method(x) obj::method

생성자의 메소드 참조

생성자를 호출하는 람다식도 메소드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass();      //  람다식
Supplier<MyClass> s = MyClass:new;              //  메소드 참조

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.

Function<Integer, MyClass> f = i -> new MyClasS(i);     //  람다식
Function<Integer, MyClass> f = MyClass:new;             //  메소드 참조

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);  //  람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new;                 //  메소드 참조

만약 배열을 생성할 때는 다음과 같이 사용한다.

Function<Integer, int[]> f = x -> new int[x];   //  람다
Function<Integer, int[]> f2 = int[]::new;       //  메소드 참조

 

예제

 

public class MethodReferences {
    public static void main(String[] args) {
//        Function<String, Integer> f = s -> Integer.parseInt(s);
        Function<String, Integer> f = Integer::parseInt;

        System.out.println(f.apply("100") + 200);

        // Supplier 입력 X, 출력 O
//        Supplier<MyClass> s = () -> new MyClass();
        Supplier<MyClass> s = MyClass::new;
        System.out.println(s.get());

        Function<Integer, MyClass> f2 = MyClass::new;
        MyClass m = f2.apply(100);
        System.out.println(m.iv);
        System.out.println(f2.apply(200).iv);

        Function<Integer, int[]> f3 = int[]::new;
        System.out.println(f3.apply(10).length);

    }
}

class MyClass {
    int iv;

    MyClass () {}

    MyClass (int iv) {
        this.iv = iv;
    }
}

 

 

references : atoz-develop.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-Expression www.notion.so/758e363f9fb04872a604999f8af6a1ae codechacha.com/ko/java8-functional-interface/ watrv41.gitbook.io/devbook/java/java-live-study/15_week

 
좋아요1
공유하기
글 요소



출처: https://dev-coco.tistory.com/29 [슬기로운 개발생활😃]

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

[8주차] 인터페이스  (0) 2022.02.06
[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

N+1 문제

첫 번째 목록을 조회하는 1번의 쿼리와 그 외 정보를 가져오기 위해서 N번의 쿼리가 실행되는 문제를 N+1 문제라고 한다.

 

아래의 코드 경우에는 주문 조회 1번, 회원 조회 N번, 배송 조회 N번이 발생한다. 주문 조회 결과 값이 2라고 가정한다면, 최악의 경우 1(주문) + 2(회원) + 2(배송) = 5 총 5번의 쿼리가 실행된다.

 

앞에서 최악의 경우라고 말한 것은 지연로딩은 기본적으로 영속성 컨텍스트에서 먼저 조회하므로 이미 조회된 경우에 쿼리를 생략한다. 만약 가져온 회원 정보가 이미 있는 경우 데이터베이스에 회원 정보를 조회하는 쿼리를 실행하지 않지만, 최악의 경우에는 매번 데이터베이스에 회원 정보를 조회하는 쿼리를 실행할 수 있다.

 

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
    // Order 조회 SQL 1번 실행 -> 2개 주문서 반환
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());

    // 2개의 주문서가 있으므로 2번 루프를 반복함
    // 하나의 SimpleOrderDto 생성할 때마다 Member, Delivery 쿼리 2번 실행
    return orders.stream().map(SimpleOrderDto::new)
        .collect(toList());
}

 

이러한 문제가 발생한 이유는 Order 엔티티와 연관 관계가 있는 Member, Delivery 엔티티가 FetchType.LAZY으로 설정되어 있기 때문이다. 그렇다면 FetchType.EAGER로 변경하면 문제를 임시적으로 해결한 것처럼 보일 수 있다. 가장 좋은 해결 방법으로는 Fetch 조인을 사용해서 튜닝을 해야 한다.

Fetch Join

SQL 조인을 활용해서 연관된 엔티티를 한번의 SQL로 모두 조회하는 기능이다.

 

public List<Order> findAll() {
    return em.createQuery(
        "select o from Order o" +
            "join fetch o.member m" +
            "join fetch o.delivery d",Order.class
    ).getResultList();
}

 

실제 수행되는 쿼리는 다음과 같다.

 

select
    order0_.order_id as order_id1_6_0_,
    member1_.member_id as member_i1_4_1_,
    delivery2_.delivery_id as delivery1_2_2_,
    order0_.delivery_id as delivery4_6_0_,
    order0_.member_id as member_i5_6_0_,
    order0_.order_date as order_da2_6_0_,
    order0_.status as status3_6_0_,
    member1_.city as city2_4_1_,
    member1_.street as street3_4_1_,
    member1_.zip_code as zip_code4_4_1_,
    member1_.name as name5_4_1_,
    delivery2_.city as city2_2_2_,
    delivery2_.street as street3_2_2_,
    delivery2_.zip_code as zip_code4_2_2_,
    delivery2_.status as status5_2_2_ 
from
    orders order0_ 
inner join
    member member1_ 
        on order0_.member_id=member1_.member_id 
inner join
    delivery delivery2_ 
        on order0_.delivery_id=delivery2_.delivery_id

JPA에서 DTO로 바로 조회

쿼리 실행 결과 값을 받을 DTO 클래스 OrderSimpleQueryDto를 생성한다.

 

@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

 

생성자를 이용해서 쿼리 결과 값을 받는다. 이 메서드의 단점은 API에서 사용할 필드 값들이 노출되고 포함되어 있다는 점이다. 따라서 API 변경에 따라 DTO 클래스와 쿼리를 수정해야 한다. 복잡한 쿼리를 조회하는 경우에는 새로운 Repository 클래스를 생성해서 그 곳에 메서드를 모아 놓는 것이 유지보수 또는 코드 파악에 도움이 된다.

 

public List<OrderSimpleQueryDto> findOrderDtos() {
    return em.createQuery(
        "select new com.jayden.shop.repository.order.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address) " +
            "from Order o " +
            "join o.member m " +
            "join o.delivery d", OrderSimpleQueryDto.class
    ).getResultList();
}

 

실제 수행되는 쿼리는 다음과 같다.

 

select
    order0_.order_id as col_0_0_,
    member1_.name as col_1_0_,
    order0_.order_date as col_2_0_,
    order0_.status as col_3_0_,
    delivery2_.city as col_4_0_,
    delivery2_.street as col_4_1_,
    delivery2_.zip_code as col_4_2_ 
from
    orders order0_ 
inner join
    member member1_ 
        on order0_.member_id=member1_.member_id 
inner join
    delivery delivery2_ 
        on order0_.delivery_id=delivery2_.delivery_id

쿼리 방식 선택 순서

  1. 엔티티를 DTO로 변환한 정보를 내려준다.
  2. 성능 최적화가 필요하면 페치 조인을 사용한다.
  3. DTO로 직접 조회하는 방법을 통해 조회하는 컬럼 개수를 줄인다.
  4. JPA가 제공하는 네이티브 SQL 또는 JDBC Template을 사용해서 SQL을 직접 사용한다.

컬렉션 조회 최적화

Fetch Join

OneToMany 관계의 엔티티들을 Fetch Join 하게 되면, 결과값으로는 One에 해당하는 엔티티 정보가 Many 개수만큼 나오게 된다.

 

orders와 order_item 두 테이블을 조인하면 4개의 레코드가 출력된다. 현재 데이터는 orders 테이블에는 행 2개, order_items 테이블에는 행 4개가 있다.

 

다음과 같이 작성한 코드를 실행하면 반환값으로 Order 엔티티 4개를 갖고 있는 리스트가 반환된다. 실제 필요한 Order 엔티티는 2개임에도 불구하고 중복을 포함해서 4개의 엔티티가 반환된다.

 

public List<Order> findAllWithItems() {
    return em.createQuery(
        "select o from Order o " +
            "join fetch o.member m " +
            "join fetch o.delivery d " +
            "join fetch o.orderItems oi " +
            "join fetch oi.item i", Order.class
    ).getResultList();
}

 

이를 해결하기 위해서 queryString 값에 distinct 키워드를 추가한다. jpa에는 distinct 키워드가 있으면 중복 엔티티를 제거해주고, DB에 날리는 쿼리에도 distinct 문장을 추가해준다.

 

public List<Order> findAllWithItems() {
    return em.createQuery(
        "select distinct o from Order o " +
            "join fetch o.member m " +
            "join fetch o.delivery d " +
            "join fetch o.orderItems oi " +
            "join fetch oi.item i", Order.class
    ).getResultList();
}

 

일대다 관계에서 Fetch Join 하게 되면 페이징 쿼리가 불가능한 단점이 있다. 페이징 쿼리를 날리기 위해서 offset, limit을 설정하면 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 한다. 실제 DB에 실행되는 쿼리에도 페이징 정보가 담기지 않고 모든 정보를 가져온다.

페이징과 한계 돌파

컬렉션은 지연 로딩으로 조회해서 페이징 처리를 한다. 지연 로딩 최적화와 페이징을 위해서 hibernate.default_batch_fetch_size 또는 @BatchSize를 적용한다.

 

주문과 주문 목록은 일대다 관계이므로 지연 로딩을 통해 조회한다. 객체 그래프로 탐색하는 순간에 주문 목록에 해당하는 쿼리를 실행하는데, 위에서 말한 옵션을 활성화하면 주문 목록 아이템을 정해진 크기만큼 한번에 조회하는 쿼리를 실행한다.

 

# order_item 조회
select
    orderitems0_.order_id as order_id5_5_1_,
    orderitems0_.order_item_id as order_it1_5_1_,
    orderitems0_.order_item_id as order_it1_5_0_,
    orderitems0_.count as count2_5_0_,
    orderitems0_.item_id as item_id4_5_0_,
    orderitems0_.order_id as order_id5_5_0_,
    orderitems0_.order_price as order_pr3_5_0_ 
from
    order_item orderitems0_ 
where
    orderitems0_.order_id in (
        ?, ?
    );

# item 조회
select
    item0_.item_id as item_id2_3_0_,
    item0_.name as name3_3_0_,
    item0_.price as price4_3_0_,
    item0_.stock_quantity as stock_qu5_3_0_,
    item0_.actor as actor6_3_0_,
    item0_.director as director7_3_0_,
    item0_.artist as artist8_3_0_,
    item0_.etc as etc9_3_0_,
    item0_.author as author10_3_0_,
    item0_.isbn as isbn11_3_0_,
    item0_.dtype as dtype1_3_0_ 
from
    item item0_ 
where
    item0_.item_id in (
        ?, ?, ?, ?
    )

 

xToOne 관계는 Fetch Join으로 조회 최적화를 적용하고, xToMany 관계에서는 지연 로딩과 hibernate.default_batch_fetch_size 또는 @BatchSize를 적용해서 조회 성능을 최적화한다.

컬렉션 조회 최적화

XToOne 관계는 Fetch Join을 이용해서 가져오고, XToMany 관계인 엔티티의 경우에 지연 로딩을 통해서 정보를 가져온다. 아래 코드에서 findOrderItemMap 메서드는 주문 아이템 목록을 1번의 쿼리를 통해 모두 가져온다. 단순히 반복문을 돌려서 주문 아이템 정보를 가져오게 되면 N + 1 문제가 발생한다.

 

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findAll() {
        List<OrderQueryDto> result = findOrders();

        List<Long> orderIds = toOrderIds(result);

        // 주문 아이템 목록을 1번의 쿼리로 가져와서 메모리에서 처리한다
        Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);

        result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

        return result;
    }
    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
            "select new com.jayden.shop.repository.order.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address) from Order o " +
                "join o.member m " +
                "join o.delivery d", OrderQueryDto.class)
            .getResultList();
    }

    private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
        List<OrderItemQueryDto> orderItems = em.createQuery(
            "select new com.jayden.shop.repository.order.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count) from OrderItem oi " +
                "join oi.item i " +
                "where oi.order.id in :orderIds", OrderItemQueryDto.class)
            .setParameter("orderIds", orderIds)
            .getResultList();

        return orderItems.stream()
            .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
    }

    private List<Long> toOrderIds(List<OrderQueryDto> result) {
        return result.stream()
                .map(o -> o.getOrderId())
                .collect(Collectors.toList());
    }

}

플랫 데이터 최적화

단 1번의 쿼리로 모든 데이터를 가져온다. 조인으로 인해서 중복 데이터가 반환되기 때문에 메모리에서 중복을 제거하는 로직이 추가되어야 한다. 조인된 쿼리를 실행하기 때문에 원하는대로 페이징 처리가 불가능하다.

 

// 주문과 주문 아이템 정보를 하나의 클래스에 Flat하게 모두 담는다.
List<OrderFlatDto> flats = orderQueryRepository.findAllByDtoFlat();

// 조인된 결과 데이터를 메모리에서 그룹핑 로직을 추가하면서 중복 데이터를 제거하고 API 스펙에 맞게 변경한다.
List<OrderQueryDto> data = flats.stream()
    .collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(),
            o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
        mapping(o -> new OrderItemQueryDto(o.getOrderId(),
            o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
    )).entrySet().stream()
    .map(e -> new OrderQueryDto(e.getKey().getOrderId(),
        e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
        e.getKey().getAddress(), e.getValue()))
    .collect(toList());

API 개발 고급 정리

Repository에서 데이터를 가져오고 나서 엔티티 조회 또는 DTO 직접 조회 두 가지 방법이 있다. 성능이 나오지 않은 경우에 Fetch Join을 이용해서 성능을 최적화한다. 다만, 컬렉션의 경우에 Fetch Join을 하게 되면 페이징 처리가 불가능해진다.

 

컬렉션은 Fetch Join 대신 지연 로딩을 유지하고, hibernate.default_batch_fetch_size 또는 @BatchSize로 최적화한다.

 

  • 엔티티 조회
  • DTO 직접 조회

권장 순서

1.엔티티 조회 방식으로 접근

  • Fetch Join으로 쿼리 수를 최적화
  • 컬렉션 최적화
    1.페이징 필요: 옵션 사용해서 최적화
    2.페이징 필요없음: Fetch Join 사용

2.엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용

3.DTO 조회 방식으로 해결이 안되면 Native SQL or Spring JdbcTemplate 사용

OSIV(Open Session In View)와 성능 최적화

OSIV(Open Session In View)

spring.jpa.open-in-view 설정은 true가 기본값이다.

 

OSIV 설정 값이 true이면, 트랜잭션 범위가 끝나도 영속성 컨텍스트를 API 응답 및 뷰 템플릿 페이지에 렌더링 할 때까지 유지한다. 이러한 이유로 뷰 템플릿 또는 API 컨트롤러에서 지연 로딩이 가능한 것이다.

 

지연 로딩은 영속성 컨텍스트가 살아 있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다. 이 부분이 장점이자 단점이다.

 

단점으로는 오랜 시간 동안에 데이터베이스 커넥션 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서 커넥션이 부족해서 장애가 발생할 수 있다.

 

OSIV 설정 값을 false로 변경하면 트랜잭션 범위에서만 영속성 컨텍스트를 유지한다. 컨트롤러에서는 이미 영속성 컨텍스트가 닫히고 커넥션 리소스를 데이터베이스에 반납했기 때문에 트랜잭션 범위 밖에서 지연 로딩을 할 수 없다.

 

장점으로는 커넥션 리소스를 짧은 기간 동안만 사용한다는 것이다.

 

단점으로는 지연 로딩 관련된 코드를 모두 트랜잭션 안에서 처리하도록 해야 한다. 뷰 템플릿에서도 지연 로딩이 동작하지 않기 때문에 트랜잭션이 끝나기 직전에 지연 로딩을 강제로 호출해야 한다.

커맨드와 쿼리 분리

실무에서 OSIV 설정 값을 끈 상태에서 복잡성을 관리하는 좋은 방법은 커맨드와 쿼리를 분리하는 것이다.

 

목표

자바의 인터페이스에 대해 학습하세요.

학습할 것 (필수)

  • 인터페이스 정의하는 방법
  • 인터페이스 구현하는 방법
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9

 

1. 인터페이스 정의하는 방법

인터페이스란?

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메소드를 갖지만 

추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 일반 메소드 또는 멤버변수를 구성원으로 가질 수 없다.

오직 추상메소드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.

* 기존에는 인터페이스에 일반 메소드를 구현할 수 없었지만, 자바 8버전부터 default 예약어를 통해 일반 메소드구현이 가능하다.

 

인터페이스의 장점

1. 개발 시간 단축
인터페이스가 작성되면 이를 사용해서 프로그램을 작성하는 것이 가능하다.
메서드를 호출하는 쪽에서는 선언부만 알면 되기 때문이다.

2. 표준화 가능
프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 하면 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

3. 서로 관계없는 클래스들 간의 관계를 맺어준다
하나의 인터페이스를 공통적으로 구현하도록 하여 관계를 맺어 줄 수 있다.

4. 독립적인 프로그래밍이 가능
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.

 

인터페이스를 작성하는 방법

인터페이스를 작성하는 것은 클래스를 작성하는 것과 동일하다. 단지 키워드로 class 대신 interface를 사용한다. 그리고 interface에도 클래스와 같이 접근제어자를 사용할 수 있다.

interface 인터페이스이름 {
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수목록);
}

일반적인 클래스의 멤버들과는 달리 인터페이스의 멤버들은 제약사항이 있다.

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. 단, static 메서드와 default 메서드는 예외(JDK 1.8부터)

생략된 제어자는 컴파일러가 자동적으로 추가해준다.

 

인터페이스 구현

인터페이스는 구현한다는 의미의 키워드 'implements'를 사용한다.

public interface Animal {
    void sound();
}
public class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }
}

여기서 중요한건 Animal인터페이스에 정의된 sound()메소드를

Dog 와 Lion 클래스에서 구현할 때 접근제어자를 public으로 했다는 것이다.

이렇게 오버라이딩 할 때는 부모의 메소드보다 넓은 범위의 접근 제어자를 지정해야한다.

 


인터페이스 레퍼런스를 통해 구현체를 사용

다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.

인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수로클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

public interface Animal {
    void sound();
}
ublic class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    public void sleep() {
        System.out.println("새근새근 잡니다.");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }

    public void hunting() {
        System.out.println("사냥을 합니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal lion = new Lion();

        dog.sound();
        lion.sound();

      //  dog.sleep();     X 
      //  lion.hunting();  X
      
      ((Dog)dog).sleep(); 	  // O
      ((Lion)lion).hunting();     // O
    }
}

dog가 바라보고 있는 것은 Animal 인터페이스이기 때문에 dog.sleep()은 호출하지 못하므로

 ((Dog)dog).sleep() 으로 캐스팅하여 사용해야 한다.

반대로 Dog dog = new Animal(); 는 선언하지 못하고 컴파일 에러가 발생한다.


인터페이스 레퍼런스를 통해 구현체를 사용

다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.

인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수로클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

public interface Animal {
    void sound();
}
ublic class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    public void sleep() {
        System.out.println("새근새근 잡니다.");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }

    public void hunting() {
        System.out.println("사냥을 합니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal lion = new Lion();

        dog.sound();
        lion.sound();

      //  dog.sleep();     X 
      //  lion.hunting();  X
      
      ((Dog)dog).sleep(); 	  // O
      ((Lion)lion).hunting();     // O
    }
}

dog가 바라보고 있는 것은 Animal 인터페이스이기 때문에 dog.sleep()은 호출하지 못하므로

 ((Dog)dog).sleep() 으로 캐스팅하여 사용해야 한다.

반대로 Dog dog = new Animal(); 는 선언하지 못하고 컴파일 에러가 발생한다.


인터페이스 상속

자바에서 다중상속은 불가능하지만, 인터페이스는 예외이다.

인터페이스는 다중상속, 즉 여러 개의 인터페이스로부터 상속 받는 것이 가능하다.

인터페이스는 인터페이스로부터만 상속받을 수 있다.

 

자동차의 다양한 주행모드의 SportsMode, EchoMode, NormalMode를 예로 들어보자.

public interface EchoMode {

    void EchoMode();
}

public interface NormalMode {

    void NormalMode();
}

public interface SportsMode {

    void SportsMode();
}

아래의 Car 클래스는  3개 (EchoMode, SportsMode, NormalMode)의 인터페이스를 상속 받는다.

public class Car implements EchoMode, SportsMode, NormalMode{
    
    @Override
    public void EchoMode() {
        System.out.println("에코 모드로 주행 합니다.");
    }

    @Override
    public void NormalMode() {
        System.out.println("기본 모드로 주행 합니다.");
    }

    @Override
    public void SportsMode() {
        System.out.println("스포츠 모드로 주행 합니다.");
    }
}

 


인터페이스 기본 메소드 (default method), 자바 8

인터페이스는 기능에 대한 선언만 가능하기 때문에, 실제 코드를 구현한 로직은 포함될 수 없다.

하지만 자바8에서 이러한 룰을 깨트리는 기능이 나오게 되었는데, 그것이 default method이다.

 

메소드 선언시에 default를 명시하게 되면 인터페이스 내부에서도 코드가 포함된 메소드를 선언할 수 있다.

 

접근제어자에서 사용하는 default와 같은 키워드이지만, 접근제어자는 아무것도 명시하지 않은 접근제어자를 default라 하며

인터페이스의 default method는 'default'라는 키워드를 명시해야 한다.

interface MyInterface {
   default void printHello() {
      System.out.println("Hello World!");
      }
}

default라는 키워드를 메소드에 명시하게 되면 인터페이스 내부라도 코드를 작성 할 수 있다.

 

 

왜 사용할까?

사실 인터페이스는 기능에 대한 구현보다는, 기능에 대한 '선언'에 초점을 맞추어서 사용 하는데, 디폴트 메소드는 왜 등장했을까?

...(중략)... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)

기존에 존재하던 인터페이스를 이용하여서 구현된 클래스를 만들고 사용하고 있는데,

인터페이스를 보완하는 과정에서 추가적으로 구현해야 할, 혹은 필수적으로 존재해야 할 메소드가 있다면,

이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어지게 된다. 이러한 경우 default 메소드를 추가하게 된다면

하위 호환성은 유지되고 인터페이스의 보완을 진행 할 수 있다.

interface MyInterface {
    default void printHello() {
        System.out.println("Hello World");
    }
}
//구현체 생성
class MyClass implements MyInterface {
}

public class DefaultMethod {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.printHello(); //실행결과 Hello World 출력
    }
}

[정리]

-interface에서도 메소드 구현이 가능하다.

-참조 변수로 함수를 호출할 수 있다.

-implements한 클래스에서 재정의가 가능하다.


인터페이스의 static 메소드, 자바 8

인스턴스 생성과 상관없이 인터페이스 타입으로 호출하는 메소드이다.

static 예약어를 사용하고, 접근제어자는 항상 public이며 생략 할 수 있다.

static method는 일반적으로 우리가 정의하는 메소드와는 다르다.

1. body가 있어야 한다.

2. implements 한 곳에서 override가 불가능하다.

public interface ICalculator {
 
    int add(int x, int y);
    int sub(int x, int y);
 
    default int mul(int x, int y) {
 
        return x * y;
    }
 
    static void print(int value) {
 
        System.out.println(value);
    }
}
public class CalcTest {
 
    public static void main(String[] args) {
 
        ICalculator cal = new Calculator();
 
        // cal.print(100); error
        ICalculator.print(100); 
        // interface의 static 메소드는 반드시 interface명.메소드 형식으로 호출
    }
}

static 메소드를 사용하는 데 주의해야할 점은 기존 클래스의 static 메소드처럼 class이름.메소드로 호출하는게 아니라

interface이름.메소드로 호출해야 한다.

100
 
Process finished with exit code 0

 

 


인터페이스의 private 메소드, 자바 9

java8 에서는 default method와 static method가 추가 되었고,

java9 에서는 private method와 private static method가 추가 되었다.

 

java8의 default method와 static method는 여전히 불편하게 만든다.

 

단지 특정 기능을 처리하는 내부 method일 뿐인데도, 외부에 공개되는public method로 만들어야하기 때문이다.

interface를 구현하는 다른 interface 혹은 class가 해당 method에 엑세스 하거나 상속할 수 있는 것을 원하지 않아도, 

그렇게 될 수 있는 것이다.

 

java9 에서는 위와 같은 사항으로 인해 private method와 private static method라는 새로운 기능을 제공해준다.

→코드의 중복을 피하고 interface에 대한 캡슐화를 유지 할 수 있게 되었다.

public interface Car {
    void carMethod();

    default void defaultCarMethod() {
        System.out.println("Default Car Method");

        privateCarMethod();
        privateStaticCarMethod();
    }

    private void privateCarMethod() {
        System.out.println("private car method");
    }

    private static void privateStaticCarMethod() {
        System.out.println("private static car method");
    }
}

DefaultCar.java 클래스 - Car 인터페이스 구현체

public class DefaultCar implements Car{

    @Override
    public void carMethod() {
        System.out.println("car method by DefaultCar");
    }
}
public class Main {
    public static void main(String[] args) {
        DefaultCar car = new DefaultCar();

        car.carMethod();
        car.defaultCarMethod();
    }
}
car method by DefaultCar
Default Car Method
private car method
private static car method

Process finished with exit code 0

 

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

람다식  (0) 2022.03.20
[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

1. 자바 상속의 특징

1-1. 상속이란?

상속이란 상위클래스에서 정의한 필드와 메서드를 하위클래스도 동일하게 사용할 수 있게 물려받는 것이다.

1-2. 상속을 사용하는 이유

코드를 재사용하기에 편하고 클래스 간 계층구조를 분류하고 관리하기 쉬워진다.

  • 영웅이 빌런들을 물리치며 세계 평화를 지키는 게임을 만든다고 가정하자.
  • 영웅은 hp, 공격력, 레벨을 가지며 상대방을 공격할 수 있고 레벨만큼 상대방의 공격력을 방어할 수 있다.
  • 빌런은 hp, 공격력 방어력을 가지며 상대방을 공격할 수 있고 단단한 방어력으로 상대방의 공격을 방어력만큼 막을 수 있다.

위 요구사항을 1차적으로 구현한 코드는 다음과 같다.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Hero.java

package study.moon.test;

public class Hero extends Unit{

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

}

위 요구사항을 보면 히어로와 빌런 둘 모두가 공통적으로 포함하고 있는 부분이 있다.

  1. hp를 가지고 있다.
  2. 공격력을 가지고 있다.
  3. 공격을 할 수 있다.
  4. 공격을 당할 수 있다.

따라서 위의 공통적인 부분을 Unit 으로 묶고 히어로와 빌런은 각각 Unit을 상속하고, 나머지를 구현하는것이 코드를 재사용할 수 있는 방법이다.

1-3. 자바 상속의 특징

1-3-1. 다중상속 금지

자바는 다중 상속을 허용하지 않는다. 예를들어 히어로와 빌런간의 이종 교배를 통하여 힐런이라는 종이 탄생했다고 치자. 힐런은 히어로와 빌런의 능력을 모두 이어받아 레벨도 있고 강력한 방어력도 가질 수 있다. 하지만 자바에서는 이러한 다중 상속을 허용하지 않는다.

public class Hellain extends Villain, Hero {
    "컴파일 에러"
}

1-3-2. 최상위 클래스 Object

자바의 모든 클래스는 최상위 클래스 Object의 서브클래스이다.

public class Main {

    public static void main(String[] args) {
        Object hero = new Hero(100,10,3);// Hero 는 Object의 서브클래스
        Object villain = new Villain(150,5,5);//Villain도 Object의 서브클래스
    }

}

2. super 키워드

2-1. super

super키워드를 사용하면 서브클래스가 수퍼클래스에 접근이 가능하다. super는 수퍼클래스의 참조변수라고 볼 수 있다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    int hp; // 보통 이렇게 하지 않지만 super를 설명하기 위해 상위클래스에서 사용한 변수명을 다시 사용한다.

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
        this.hp = 1000; // 빌런만 가지고있고 유닛은 없는 hp를 1,000으로 설정한다.
        super.hp = 10000; //유닛이 공통으로 가지고있는 hp를 10,000으로 설정한다.
    }

}

2-2. super()

super()를 사용하면 수퍼클래스의 생성자를 호출할 수 있다.

다시 정상적인 빌런과 유닛의 코드를 보자.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) { // 해당 생성자가 빌런에서 super(hp,attackPoint)로 호출된다.
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint); //Unit의 생성자를 호출한다.
        this.defensePoint = defensePoint;
    }

}

빌런이 super(hp,attackPoint)를 호출하였다.
즉, 수퍼클래스의 Unit(hp, attackPoint)생성자를 호출하여 초기화했다는 뜻이다.
그러면 이제 빌런은 추가적인 방어력 부분만 의존성으로 받아와서 새롭게 셋팅하면 된다.

!!! 수퍼클래스의 생성자의 인자가 없다면 서브클래스에서 super()를 작성하지 않아도 자동으로 컴파일시에 추가된다.

3. 메소드 오버라이딩

수퍼클래스가 가지고있는 메서드를 서브클래스에서 새롭게 다른 로직으로 정의하고 싶을 때 사용하는 문법이다.
상속관계에 있는 클래스간에 같은 이름의 메서드를 정의하는 문법을 오버라이딩이라고 한다.
오버라이딩 어노테이션은 생략할 수도 있다.

빌런은 공격을 받을때 히어로와는 다르게 방어력의 수치만큼 공격피해가 덜 들어와야 한다.
따라서 유닛이 공통으로 가지고있는 공격받는 로직을 새롭게 작성해주어야 한다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    @Override
    public void attackedBy(Unit unit) {// Unit에 정의되어있는 메서드를 재정의했다.
        this.hp -= (unit.attackPoint - this.defensePoint); //빌런은 일반적인 유닛과는 다르게 방어력만큼 공격이 덜 들어온다.
    }

    @Override
    public String toString() {
        return "Villain{" +
            "hp=" + hp +
            ", attackPoint=" + attackPoint +
            ", defensePoint=" + defensePoint +
            '}';
    }
}

4. 다이나믹 디스패치

컴파일타임에는 알 수 없는 메서드의 의존성을 런타임에 늦게 바인딩 하는것이다.

다음 코드를 보도록 하자.

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Hero hero = new Hero(100,10,3);
        Villain villain = new Villain(150,5,5);
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

다음 코드는 각각의 객체가 어떤 메서드를 호출할 지 정확하게 예측이 가능하다.
히어로는 Hero의 attack()을 호출 할 것이고
빌런은 Villain의 attack()을 호출 할 것이다.
이것은 모두 컴파일 타임 에 파악할 수 있다.
컴파일된 class파일을 통해 확인해보도록 하자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #19         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #20                 
      40: aload_1
      41: invokevirtual #26                 
      44: getstatic     #20                 
      47: aload_2
      48: invokevirtual #26                 
      51: return

장황한 주석들을 삭제하고 두 메서드가 관련된 부분만 살펴보도록 하자.
29행과 34행을 살펴보면 해당 인스턴스는 정확하게 어떤 클래스의 메서드를 호출 할 것인지 명확하게 알 수 있다.
hero는 Hero클래스의 attack을 호출, villain은 Villain클래스의 attack을 호출한다.

하지만 나는 다형성을 적용하여 코드를 조금 더 유연하게 작성하고 싶다.
다음과 같이 코드를 변경해보자

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

이렇게 코드가 변경된다면 과연 어떻게될까?
컴파일된 class 파일을 살펴보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #21                 
      40: aload_1
      41: invokevirtual #27                 
      44: getstatic     #21                 
      47: aload_2
      48: invokevirtual #27                 
      51: return

이번에도 딱 두 부분의 메서드가 컴파일 타임에 어떻게 바인딩되어있는지 확인해보도록 하자
29행과 34행을 보면 해당 메서드는 Unit의 attack을 사용하기로 결정되어있다.
그렇다면 빌런과 히어로 모두 유닛의 attack을 사용하는것일까??
그렇지 않다. 다음 결과물을 보도록 하자.

Main.java

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}
Hero{hp=98, attackPoint=10, level=3}
Villain{hp=145, attackPoint=5, defensePoint=5} // 유닛의 메서드가아닌 빌런의 메서드가 호출된 결과이다.

위의 main 함수의 결과물이다.

빌런이 일반 유닛이라면 방어력의 영향을 받지 않고 히어로의 공격 10을 온전히 받아냈어야했다.
그러나 오버라이딩 된 빌런의 계산을 따랐고 그 결과 히어로의 공격력 10에서 방어력 5를 뺀 데미지만 입게 되었다.
히어로도 레벨의 영향을 받아 레벨만큼 공격을 덜받았다.

이처럼 컴파일 타임에는 메서드의 클래스타입이 정해져있지 않지만 런타임에 정해져서 메서드를 호출하는 것을
동적 dispatch 라고 한다.

많은 블로그 글을 둘러보면서 확인한 결과, 대부분의 블로그의 글들이 구현체가 없는 인터페이스, 혹은 추상클래스의 메서드를 호출할 때 클래스 타입에 따라 동적으로 메서드가 결정되는것이라고 설명해놓았다. 그런데 해 본 결과 굳이 추상클래스나 인터페이스가 아니어도 동적 디스패치는 발생할 수 있다. 수퍼클래스의 메서드를 오버라이딩해도 동적 디스패치가 가능하다.

5. 더블 디스패치

  • 히어로는 레벨이 올라 수퍼 히어로,하이퍼 히어로로 업그레이드할 수 있게 하고싶다.
  • 빌런도 일정 시간이 지나면 수퍼빌런, 하이퍼 빌런으로 업그레이드할 수 있게 만들고 싶다.
  • 그에 맞춰서 각각 업그레이드 한 상태에서 상대방을 공격할 때의 로직을 if문을 사용하지 않고 유기적으로 작성해주고싶다.
  • 어떻게 작성해야 할까?

ex)
수퍼히어로 -> 빌런
하이퍼히어로 -> 빌런
수퍼빌런 -> 히어로
하이퍼빌런 -> 히어로
모두 다른 로직을 작성하고싶다!

코드를 다음과 같이 수정해보자. 그러면 if문은 사용하지 않고도 분기처리를 진행할 수 있다.

주석에 적힌 부분을 따라가면 2번 동적 디스패치가 발생했다는 사실을 알 수 있다.

Unit.java

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

}

Hero.java

public abstract class Hero extends Unit {

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

    //1. goto  hyperHero.attack()  or  superHero.attack()
    public abstract void attack(Villain villain);  

    public void isAttackedBy(SuperVillain superVillain) {
        hp -= (superVillain.attackPoint * 2 - level);
    }

    public void isAttackedBy(HyperVillain hyperVillain) {
        hp -= (hyperVillain.attackPoint);
    }
}

HyperHero.java

public class HyperHero extends Hero {

    public HyperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }

}

SuperHero.java

public class SuperHero extends Hero{

    public SuperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }
}

Villain.java

public abstract class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    //1. goto  hyperVillain.attack()  or  superVillain.attack() 
    public abstract void attack(Hero hero);

    public void isAttackedBy(SuperHero superHero) {
        hp -= (superHero.attackPoint + superHero.level - defensePoint);
    }

    public void isAttackedBy(HyperHero hyperHero) {
        hp -= (hyperHero.level * 3);
    }
}

HyperVillain.java

public class HyperVillain extends Villain {

    public HyperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }
}

SuperVillain.java

public class SuperVillain extends Villain{

    public SuperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }

}

Main.java

public class Main {

    public static void main(String[] args) {
        Hero superHero = new SuperHero(100,20,5);
        Hero hyperHero = new HyperHero(100,5,10);
        Villain superVillain = new SuperVillain(200,10,5);
        Villain hyperVillain = new HyperVillain(150,15,10);
        superHero.attack(superVillain);
        superHero.attack(hyperVillain);
        hyperHero.attack(superVillain);
        hyperHero.attack(hyperVillain);
        superVillain.attack(superHero);
        superVillain.attack(hyperHero);
        hyperVillain.attack(superHero);
        hyperVillain.attack(hyperHero);

        System.out.println(superHero);
        System.out.println(hyperHero);
        System.out.println(superVillain);
        System.out.println(hyperVillain);
    }
}

컴파일 된 클래스 파일을 확인하면서 어떻게 동적 디스패치가 일어났는지 확인해보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        20
       8: iconst_5
       9: invokespecial #9                  
      12: astore_1
      13: new           #12                 
      16: dup
      17: bipush        100
      19: iconst_5
      20: bipush        10
      22: invokespecial #14                 
      25: astore_2
      26: new           #15                 
      29: dup
      30: sipush        200
      33: bipush        10
      35: iconst_5
      36: invokespecial #17                 
      39: astore_3
      40: new           #18                 
      43: dup
      44: sipush        150
      47: bipush        15
      49: bipush        10
      51: invokespecial #20                 
      54: astore        4
      56: aload_1
      57: aload_3
      58: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      61: aload_1
      62: aload         4
      64: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      67: aload_2
      68: aload_3
      69: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      72: aload_2
      73: aload         4
      75: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      78: aload_3
      79: aload_1
      80: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      83: aload_3
      84: aload_2
      85: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      88: aload         4
      90: aload_1
      91: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      94: aload         4
      96: aload_2
      97: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
     100: getstatic     #32                 
     103: aload_1
     104: invokevirtual #38                 
     107: getstatic     #32                 
     110: aload_2
     111: invokevirtual #38                
     114: getstatic     #32                 
     117: aload_3
     118: invokevirtual #38                 
     121: getstatic     #32                 
     124: aload         4
     126: invokevirtual #38                 
     129: return
}

다음과 같은 방식으로 코드를 작성하면 조금 더 유연한 코드가 된다. 업그레이드 된 히어로 빌런이 추가된다면 해당 부분을 추가해서 넣어주기만 하면 된다. 디자인 패턴으로는 다음과 같은 패턴을 방문자 패턴이라고 부른다고 한다.

6. 추상클래스

구체적이지 않은 클래스를 말한다. 예를들어 구체적인 클래스가 히어로, 빌런이라면 추상적인 클래스는 유닛이 될 수 있다.
공통된 부분으로 묶기에는 적당하지만 구현을 하지는 않을 클래스를 만들 때 추상클래스를 이용한다.

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public abstract void attack(Unit unit);

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }
}
  • 클래스 앞에 abstract 키워드를 이용하면 해당 클래스는 추상클래스가 된다.
  • 추상클래스는 추상메서드를 작성할 수 있다. (추상메서드란, 구현부가 없는 메서드이다.)
  • 추상메서드는 메서드의 반환형 앞에 absract를 붙이면 된다.
  • 추상클래스는 인스턴스를 생성할 수 없다.
  • 추상 클래스를 상속받은 클래스는 수퍼클래스가 가지고있는 추상메서드를구현하지 않으면 추상클래스가 된다.

7. final

final은 다시 무언가를 정의내리는것을 막는 키워드이다.

  • class
    • 클래스의 상속을 막는다.
  • variable
    • 변수의 재할당을 막는다.
  • method
    • 메서드의 오버라이딩을 막는다.

많은 사람들이 오해하고 있는 부분중에 하나가 바로 변수에 final을 사용하면 불변한다는 것이다.
그러나 대상은 사실 변할 수 있다.예제를 살펴보자.

import java.util.ArrayList;
import java.util.List;

class Main {

    public static void main(String[] args) {
        final List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println(list); // [1, 2, 3, 4, 5]
    }
}

list 내부의 값이 0개에서 5개로 증가했다.
이처럼 final에서의 불변은 대상이 불변하는것이 아니라 새롭게 할당하는것을 막는다는것을 의미한다.

그렇다면 final 키워드는 왜 사용할까?

  • 우리의 기억력이 완벽하지 않고, 우리 코드의 의도가 분명하지 않기때문에 사용한다.
  • 우리는 항상 실수하기 때문에 final을 사용하여 미리 실수를 차단할 수 있는 방안은 모두 사용해야한다.

8. Object

  • 자바의 최상위 클래스이다.
  • 따로 어디서 상속받지 않더라도 Obejct는 모든 클래스의 최상위 클래스이기 때문에 내가 클래스를 생성하면 그 클래스에는 자동으로 object의 기본메서드가 포함되어있다.

Object 클래스의 메서드

http://www.tcpschool.com/java/java_api_object

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

람다식  (0) 2022.03.20
[8주차] 인터페이스  (0) 2022.02.06
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

클래스와 객체

클래스는 객체지향 프로그래밍(Object-oriented programming)에서 객체를 생성하기 위해 상태(state)와 행동(behavior)을 정의하는 일종의 설계도이다. 여기서 객체란 어플리케이션의 기능을 구현하기 위해 서로 협력하는 개별적인 실체로써 물리적일 수도 있고 개념적일 수도 있다. 앞으로 배울 객체지향의 4대 특성(추상화, 캡슐화, 상속, 다형성)을 통해 프로그램 개발 및 유지보수를 더욱 쉽고 빠르게 할 수 있을 것이다.

클래스 정의

객체의 상태와 행동이 정의된 하나의 클래스로 비슷한 구조를 갖되 상태는 서로 다른 여러 객체를 만들 수 있다. 그렇다면 어떻게 정의해야 할까? 먼저 클래스의 구조를 살펴보자.

  • 필드(field) - 필드는 해당 클래스 객체의 상태 속성을 나타내며, 멤버 변수라고도 불린다. 여기서 초기화하는 것을 필드 초기화 또는 명시적 초기화라고 한다.
    • 인스턴스 변수 - 이름에서 알 수 있듯이 인스턴스가 갖는 변수이다. 그렇기에 인스턴스를 생성할 때 만들어진다. 서로 독립적인 값을 갖으므로 heap 영역에 할당되고 gc에 의해 관리된다.
    • 클래스 변수 - 정적을 의미하는 static키워드가 인스턴스 변수 앞에 붙으면 클래스 변수이다. 해당 클래스에서 파생된 모든 인스턴스는 이 변수를 공유한다. 그렇기 때문에 heap 영역이 아닌 static 영역에 할당되고 gc의 관리를 받지 않는다. 또한 public 키워드까지 앞에 붙이면 전역 변수라 볼 수 있다.
  • 메서드(method) - 메서드는 해당 객체의 행동을 나타내며, 보통 필드의 값을 조정하는데 쓰인다.
    • 인스턴스 메서드 - 인스턴스 변수와 연관된 작업을 하는 메서드이다. 인스턴스를 통해 호출할 수 있으므로 반드시 먼저 인스턴스를 생성해야 한다.
    • 클래스 메서드 - 정적 메서드라고도 한다. 일반적으로 인스턴스와 관계없는 메서드를 클래스 메서드로 정의한다.
  • 생성자(constructor) - 생성자는 객체가 생성된 직후에 클래스의 객체를 초기화하는 데 사용되는 코드 블록이다. 메서드와 달리 리턴 타입이 없으며, 클래스엔 최소 한 개 이상의 생성자가 존재한다.
  • 초기화 블록(initializer) - 초기화 블록 내에서는 조건문, 반복문 등을 사용해 명시적 초기화에선 불가능한 초기화를 수행할 수 있다.
    • 클래스 초기화 블록 - 클래스 변수 초기화에 쓰인다.
    • 인스턴스 초기화 블록 - 인스턴스 변수 초기화에 쓰인다.
    • 클래스 변수 초기화: 기본값 → 명시적 초기화 → 클래스 초기화 블록
      인스턴스 변수 초기화: 기본값 → 명시적 초기화 → 인스턴스 초기화 블록 → 생성자

위의 구조를 토대로 클래스를 정의한다면 다음과 같이 코드를 작성할 수 있다.

class Class {               // 클래스
    String constructor;
    String instanceVar;     // 인스턴스 변수
    static String classVar; // 클래스 변수

    static {                // 클래스 초기화 블록
        classVar = "Class Variable";
    }

    {                       // 인스턴스 초기화 블록
        instanceVar = "Instance Variable";
    }

    Class() {                // 생성자
        constructor = "Constructor";
    }

    void instanceMethod() {       // 인스턴스 메서드
        System.out.println(instanceVar);
    }

    static void classMethod() {   // 클래스 메서드
        System.out.println(classVar);
    }
}

접근 제어자 - public, protected, default, private
그 외 - static, final, abstract, transient, synchronized, volatile etc.

static이나 public같은 키워드를 제어자(modifier)라고 하며, 클래스나 멤버 선언 시 부가적인 의미를 부여한다.

  • 접근 제어자 - 접근 제어자는 해당 클래스 또는 멤버를 정해진 범위에서만 접근할 수 있도록 통제하는 역할을 한다. 클래스는 public과 default밖에 쓸 수 없다. 범위는 다음과 같다. 참고로 default는 아무것도 덧붙이지 않았을 때를 의미한다.
  • static - 변수, 메서드는 객체가 아닌 클래스에 속한다.
  • final
    • 클래스 앞에 붙으면 해당 클래스는 상속될 수 없다.
    • 변수 또는 메서드 앞에 붙으면 수정되거나 오버라이딩 될 수 없다.
  • abstract
    • 클래스 앞에 붙으면 추상 클래스가 되어 객체 생성이 불가하고, 접근을 위해선 상속받아야 한다.
    • 변수 앞에 지정할 수 없다. 메서드 앞에 붙는 경우는 오직 추상 클래스 내에서의 메서드밖에 없으며 해당 메서드는 선언부만 존재하고 구현부는 상속한 클래스 내 메서드에 의해 구현되어야 한다. 상속과 관련된 내용은 6주차에 다룰 예정이다.
  • transient - 변수 또는 메서드가 포함된 객체를 직렬화할 때 해당 내용은 무시된다.
  • synchronized - 메서드는 한 번에 하나의 쓰레드에 의해서만 접근 가능하다.
  • volatile - 해당 변수의 조작에 CPU 캐시가 쓰이지 않고 항상 메인 메모리로부터 읽힌다.

객체 생성

클래스에서 객체를 생성하려면 아래와 같이 new키워드를 생성자 중 하나와 함께 사용하면 된다.

public class Point {
    private int x = 1;
    private int y = 2;

    Point() {}  // 기본 생성자

    Point(int x, int y) {
        setX(x);
        setY(y);
    }

    public int getX() {
        return x;
    }

    public int getY() {

        return y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }
}
public class PointMain {
    public static void main(String[] args) {
        Point p1 = new Point();
        Point p2 = new Point(3, 4);
        System.out.println("" + p1.getX() + ", " + p1.getY());  // 1, 2
        System.out.println("" + p2.getX() + ", " + p2.getY());  // 3, 4
        p1.setX(3);
        p1.setY(4);
        System.out.println("" + p1.getX() + ", " + p1.getY());  // 3, 4
    }
}

new키워드는 새 객체에 메모리를 할당하고 해당 메모리에 대한 참조값을 반환하여 클래스를 인스턴스화한다. 일반적으로 객체가 메모리에 할당되면 인스턴스라 부른다. 참고로 인스턴스 p1과 p2은 서로 다른 생성자에 의해 생성되었다.

메서드 정의

public int getX() {
    return x;
}

public int getY() {
    return y;
}

public void setX(int x) {
    this.x = x;
}

public void setY(int y) {
    this.y = y;
}

  • 접근 제어자 및 기타 제어자 - 해당 메서드에 접근할 수 있는 범위를 명시하거나 위에서 언급했듯이 부가적인 의미를 부여한다.
  • 반환 타입 - 메서드가 모든 작업을 수행한 뒤에 반환할 타입을 명시한다.
  • 메서드 이름 - 메서드명은 동사여야 하고 lowerCamelCase를 따르며 뜻이 명확해야 한다. 위의 메서드는 getter/setter 메서드이다.
  • 매개변수 리스트 - 메서드에서 사용할 매개변수들을 명시한다.
  • 메서드 시그니처 - 컴파일러는 메서드 시그니처를 보고 오버로딩(overloading)을 구별한다. 물론 리스트의 순서도 동일해야 한다.

생성자 정의

Point() {}  // 기본 생성자

Point(int x, int y) {
    setX(x);
    setY(y);
}

앞서 말했듯이 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 생성한다. 하지만 기본 생성자가 아닌 다른 형태의 생성자만 명시했다면 기본 생성자는 컴파일시에 생성되지 않는다.

this 키워드

public void setX(int x) {
    this.x = x;
}

public void setY(int y) {
    this.y = y;
}

this키워드는 인스턴스 자신을 가르킨다. 위 코드에서 this를 사용함으로써 지역변수 x, y와 구분할 수 있다. 당연한 말이지만 클래스 메서드에서는 this를 쓸 수 없다. 왜냐하면 인스턴스가 생성되지 않았을 수도 있기 때문이다.

this()는 해당 클래스 생성자를 호출할 수 있다. 그렇기 때문에 생성자를 재사용하는 데 쓰인다. (생성자 체이닝)

public class Point {
    int x;
    int y;
    int z;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    Point(int x, int y, int z) {
        this(x, y);
        this.z = z;
    }
}

과제

  • int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
  • int value, Node left, right를 가지고 있어야 합니다.
public class Node {
    private int value;
    private Node left;
    private Node right;

    Node(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }

    public Node addLeftNode(int value) {
        Node node = new Node(value);
        setLeft(node);
        return node;
    }

    public Node addRightNode(int value) {
        Node node = new Node(value);
        setRight(node);
        return node;
    }

    public int getValue() {
        return value;
    }

    public Node getLeft() {
        return left;
    }

    public Node getRight() {
        return right;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public void setRight(Node right) {
        this.right = right;
    }
}
  • BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
  • DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
import java.util.*;

public class BinaryTree {
    public List<Integer> bfsList = new ArrayList<>();
    public List<Integer> dfsList = new ArrayList<>();

    public void bfs(Node node) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(node);
        while (!queue.isEmpty()) {
            Node n = queue.poll();
            bfsList.add(n.getValue());
            if (n.getLeft() != null) {
                queue.offer(n.getLeft());
            }
            if (n.getRight() != null) {
                queue.offer(n.getRight());
            }
        }
    }

    public void dfs(Node node) {
        if (node == null) return;
        if (node.getLeft() != null) {
            dfs(node.getLeft());
        }
        dfsList.add(node.getValue());
        if (node.getRight() != null) {
            dfs(node.getRight());
        }
    }
}

package week5.binarytree;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.util.*;

class BinaryTreeTest {
    static BinaryTree tree;
    static Node root;

    @BeforeAll
    static void createBinaryTree() {
        tree = new BinaryTree();
        root = new Node(1);

        Node temp = root.addLeftNode(2);
        temp.addRightNode(6);
        temp = temp.addLeftNode(3);
        temp.addLeftNode(4);
        temp.addRightNode(5);

        temp = root.addRightNode(7);
        temp.addLeftNode(8);
        temp.addRightNode(9);
    }

    @Test
    void bfs() {
        tree.bfs(root);
        List<Integer> answer = Arrays.asList(1, 2, 7, 3, 6, 8, 9, 4, 5);
        assertArrayEquals(answer.toArray(), tree.bfsList.toArray());
    }

    @Test
    void dfs() {
        tree.dfs(root);
        List<Integer> answer = Arrays.asList(4, 3, 5, 2, 6, 1, 8, 7, 9);
        assertArrayEquals(answer.toArray(), tree.dfsList.toArray());
    }
}

package 키워드

패키지란

자바에서 Package는 클래스, 서브 패키지 그리고 인터페이스로 이루어진 그룹을 캡슐화하는 매커니즘을 말한다. 그리고 다음과 같은 경우를 위해 사용된다:

  1. 이름 충돌 방지(Preventing naming conflicts)
  2. classes, interfaces, enumerations and annotations를 더 쉽게 찾고, 사용하기 위해
  3. 접근 제어
  4. 데이터 캡슐화로 간주되기도 한다.

패키지가 동작하는 방식

패키지는 directory structure와 밀접한 관련이 있다. 예를 들어 college.staff.cse라는 패키지 명이 있다면, 3개의 디렉토리(college, staff, cse)가 존재하며 cse는 staff안에 staff는 college 디렉토리 안에 위치한다.

또한, college 디렉토리는 CLASSPATH 변수를 통해서 접근할 수 있다.

패키지 네이밍 관습

domain name의 역순으로 명명된다. (i.e, org.geeksforgeeks.practice, college.tech.cse, college.tech.ee)

패키지에 클래스 추가하기

  1. 패키지 안에 클래스를 생성한다.
  2. 클래스를 정의하는 프로그램 코드 맨 윗줄에 패키지 이름을 선언한다.
/* dog.java */
package Animal

public class Dog {
    ...
}

서브 패키지

패키지 안에서 사용되는 또 다른 패키지를 말하며 import 선언을 통해 사용할 수 있다. 사윙 패키지가 서브 패키지의 멤버에 접근할 수 있는 특별한 권한은 따로 존재하지 않는다.

import java.util.*;

패키지의 종류

  1. Built-in Package
  2. User-defined Package

Built-in Package

수 많은 패키지가 기본적으로 자바에서 제공되며 그중 자주 쓰이는 몇가지의 용도를 정리해볼 수 있다.

  1. java.lang: 언어의 기본적인 클래스를 제공하기 때문에 항상 자동으로 import되는 패키지이다. (e.g. 데이터 원시 타입, 수학 연산 등을 포함하는 클래스)
  2. java.io: Input/Output을 관리하는 클래스를 포함하는 패키지
  3. java.util: 자료구조나 시간과 같은 유틸리티 클래스를 포함하는 패키지

User-defined Package

말 그래도 유저에 의해 정의된 패키지를 말한다.

import 키워드

서브 클래스 내부로 접근하기

// java안에 있는 util 패키지 내에 모든 클래스에 접근하기
import java.util.*;

// java 안에 있는 util 패키지 중 Vector 클래스에 접근하기
import java.util.Vector;

패키지를 정의하고 사용하기

  1. 처음 만들어진, 최상위에 위치하는 디렉토리의 이름은 패키지 이름과 동일해야한다.
  2. 그 아래 생성된 디렉토리와 클래스에는 .으로 구분하며 접근할 수 있다.
 `- example
    |- MyClass.java
    `- PrintName.java
/* MyClass.java */
package example;

public class MyClass {
    public void getNames(String s) {
        System.out.println(s);
    }
}

/* PrintName.java */
package main.java.example;

import example.MyClass;

public class PrintName 
{
   public static void main(String args[]) 
   {       
      String name = "GeeksforGeeks";
      MyClass obj = new MyClass();
      
      obj.getNames(name);
   }
}

static import

자바 버전 5 이후부터 소개된 기능이다. 한 클래스에 정의된 멤버들(fields와 methods)를 클래스를 특정하지 않고 public static으로 자바 코드 안에서 사용될 수 있게 만들어주는 기능을 말한다.

import static java.lang.System.*;

class StaticImportDemo {
    public static void main(String args[]) {
        out.println("Hello world");
    }
}

이름 충돌 다루기

예를 들어 상황을 살펴보자. java.util과 java.sql 패키지는 둘다 Date라는 이름의 클래스를 포함한다. 따라서 다음과 같은 코드는 컴파일 에러를 발생시킨다.

import java.util.*;
import java.sql.*;

Date today; // java.util과 java.sql 패키지 모두 
            // Date라는 클래스를 포함하기에 
            // 컴파일러는 어느것을 지칭하는지를 구분할 수 없다.

한 쪽에서만 Date 클래스를 사용해야 한다면 다음과 같은 코딩이 솔루션이 될 수 있다.

import java.util.Date
import java.sql.*;

하지만 양 쪽에서 써야한다면 변수 선언 시에 풀 패키지 명으로 선언해줘야 한다.

java.util.Date deadLine = new java.util.Date();
java.sql.Date today = new java.sql.Date();

Packages in Java - GeeksforGeeks

클래스패스

그림 출처 - 알짜배기 프로그래머 블로그

위 그림에서 클래스 로더는 JVM에 바이트 코드(.class)를 불러오는 역할을 한다. 그렇다면 클래스 로더는 어떻게 바이트 코드의 위치를 알 수 있었을까?

JVM의 클래스 로더는 JVM이 시작될 때 CLASSPATH 환경 변수를 호출하여 CLASSPATH 환경 변수에 설정되어 있는 디렉토리를 파악할 수 있었다.

그렇다면 클래스패스는 무엇인가?

클래스패스는 클래스가 위치하고 있는 경로를 말한다. 더 명확하게 말해보자면 JVM이 프로그램을 실행할 때, 클래스 파일을 찾는데 기준이 되는 파일 경로를 말한다.

기본적으로는 java 명령어가 실행된 위치를 default class path로 갖는다. 하지만 CLASSPATH 환경 변수 설정이나 커맨드 라인에서 java 명령어에 -classpath나 -cp 명령을 이용해 설정할 수 있다.

위 그림에서 볼 수 있듯이

  1. Java Source code(.java)를 컴파일하면
  2. Byte Code(.class) 형태로 변환된다.
  3. 이 클래스 파일(Byte Code)에 포함된 명령을 사용하려면 해당 파일을 찾을 수 있어야 한다.
  4. 그 경로를 찾기 위해 CLASSPATH 환경 변수에 저장된 클래스패스를 사용한다.

자바 클래스패스(classpath)란? - 코딩하는 오징어 블로그

CLASSPATH 환경변수

CLASSPATH 환경 변수는 앞에서도 언급했듯이 .class 파일을 찾기 위한 경로가 저장되어 있는 환경 변수를 말한다. 더 정확히는 .class 파일이 포함된 디렉토리와 파일을 세미콜론(;)으로 구분한 목록이다.

  1. java runtime은 이 CLASSPATH 환경 변수에 저장된 경로를 모두 검색해서 특정 클래스에 대한 코드가 포함된 .class 파일을 찾는다.
  2. 찾으려는 클래스 코드가 포함된 .class 파일을 찾으면 해당 파일을 사용한다.

Windows에서 CLASSPATH 환경 변수 설정하기

-classpath 옵션

커맨드 라인에서 java 명령어를 실행할 때 -classpath나 -cp를 붙여 클래스패스를 특정할 수 있다. 지금부터는 그 방법과 커맨드 라인에서 CLASSPATH를 설정할 수 있는 또 하나의 방법을 소개하겠다.

Use -classpath or -cp

C:> sdkTool -classpath classpath1;classpath2...

Use set CLASSPATH

C:> set CLASSPATH=classpath1;classpath2...
  1. sdkTool
    • java, javac, javadoc과 같은 커맨드 라인 툴이 해당한다.
  2. classpath1;classpath2
    • .jar, .zip, .class 파일이 위치하는 클래스패스를 뜻한다.
    • 각 클래스패스는 파일 이름이나 디렉토리 이름으로 끝나야한다.
  3. 다양한 클래스패스를 갖는다면 세미콜론(;)으로 구분한다.
  4. default class path는 현재 디렉토리를 가리킨다.
    • 다만, 다른 클래스 패스를 추가하려고 -classpath 옵션이나 set을 사용한다면 .을 맨 앞에 붙여 현재 위치를 클래스패스 목록에 추가할 수 있다.
  5. 디렉토리, 아카이브(.zip or .jar files)나 *이 아닌 클래스패스는 무시된다.

Setting the class path - Oracle Java SE Document

접근지시자

자바에서는 클래스, 인터페이스나 멤버에 대한 접근을 제어할 수 있는 접근 지시자(Access Modifier)라는 것을 제공한다. Effective Java에서는 Access Control Mechanism으로 표현하고 있다.

선언 위치에 따라 위에서 언급한 항목들에 대해 접근성이 정해진다는 것이다.

top-level(non-nested) classes and interfaces, there are only two possible access levels:

  • package-private
  • public

For members (fields, methods, nested classes, and nested interfaces), there are four possible access levels, listed here in order of increasing accessibility:

  • private: 해당 멤버가 선언된 클래스(top-level class) 안에서만 접근할 수 있다.
  • package-private: 해당 멤버가 선언된 패키지 안에서만 접근할 수 있다.
  • protected:
    • 해당 멤버가 선언된 클래스의 자식 클래스에서 접근할 수 있다.
    • 해당 멤버가 선언된 패키지 내의 어느 클래스에서든 접근할 수 있다.
  • public: 어디서든 접근이 가능하다.

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

[8주차] 인터페이스  (0) 2022.02.06
[6주차] 상속  (0) 2022.01.30
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10
[2주차] 자바 데이터 타입, 변수 그리고 배열  (0) 2022.01.02

과제 0. JUnit 5 학습하세요.

  • 인텔리J, 이클립스, VS Code에서 JUnit 5로 테스트 코드 작성하는 방법에 익숙해 질 것.
  • 이미 JUnit 알고 계신분들은 다른 것 아무거나!
  • 더 자바, 테스트 강의도 있으니 참고하세요~

JUnit5의 Life-Cycle

  • @BeforeAll : @Test 메소드들이 실행되기 전에 실행
  • @BeforeEach : 각각의 @Test 메소드가 실행되기 전에 실행
  • @AfterEach : 각각의 @Test 메소드가 실행된 후에 실행
  • @AfterAll : @Test 메소드들이 실행된 후에 실행
import org.junit.jupiter.api.*;

public class JUnit5Test {
    @BeforeAll
    static void beforeAll() {
        System.out.println("BeforeAll Test");
    }
    @BeforeEach
    static void beforeEach(){
        System.out.println("BeforeEach");
    }

    @Test
    @DisplayName("테스트 1☆")
    static void testing() {
        System.out.println("testing");
    }

    @AfterEach
    static void afterEach() {
        System.out.println("AfterEach");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("AfterAll");
    }

JUnit5 Feature

  • @DisplayName : 한글, 스페이스,이모지를 통해 테스트 이름의 표기가능
  • @Nested : 계층 구조 테스트가 가능하게 지원
  • @ParameterizedTest : 여러개의 테스트 데이터를 매개변수 형태로 간편하게 사용 가능, NullSource, ValueSource, EmptySource, CsvSource, EnumSource, MethodSource등 최소 하나의 소스 어노테이션이 필요
//DisplayName을 이용한 테스트 이름 작성
@Test
@DisplayName("테스트 1☆")
static void testing() {
	System.out.println("testing");
}

//Nested를 이용한 계층 구조 테스트
@Nested
@DisplayName("people")
class People {
	@Nested
  @DisplayName("man")
  class Man {
	  @Test
    static void manTest() {
	    System.out.println("man");
    }
  }
	@Nested
  @DisplayName("woman")
  class Woman {
	  @Test
    static void womanTest() {
	    System.out.println("woman");
    }
  }
}

//ParameterizedTest를 이용한 매개변수이용 
@ParameterizedTest
@ValueSource(ints = {1,2,3,4,5})
static void isOdd(int num){
	assertTrue(Numbers.isOdd(num));
}

JUnit Assert

기존 JUnit4는 assert 메소드가 하나라도 실패하면 다음 assert를 실행하지 않았다. 하지만 JUnit5는 assertAll이라는 메소드를 통해 여러개의 assert를 실행하게 하여 실패하더라도 모든 결과를 확인할 수 있게 지원하였다.

@Test
static assertAllTest() {
	int num = 10;
	assertAll("assertAll test",
				  () -> assertEquals(10,num),
          () -> assertEquals(13,num+5),
          () -> assertEquals(15,num+5)
	);
}

또한, JUnit4의 경우 라이브러리를 이용해 예외 검증이 가능했다면, JUnit5는 assertThrows를 이용해 예외 검증이 가능하게 되었다.

@Test
static void assertThrowsTest() {
	Exception exception = assertThrows(
				  IllegalArgumentException.class, () -> {
					    throw new IllegalArgumentException("a message");
					}
	);
  assertEquals("message",exception.getMessage());
}

그리고, 어노테이션을 이용해 테스트 실행시간을 확인한것에 반해 assertTimeout을 이용해 테스트 실행시간에 대한 검증이 가능하게 되었다.

@Test
static void assertTimeoutTest() {
	assertTimeout(ofSeconds(1), () -> {
	  // 1초 이내에 수행해야함
  });
}

과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.

  • 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
  • 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
  • Github 자바 라이브러리를 사용하면 편리합니다.
  • 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.*;

public class GitHubIssue {
    //personal token need to secret
    private static final String MY_PERSONAL_TOKEN = "MY_SECRET_TOKEN";

    public static void main(String[] args) throws IOException {
        GitHub github = new GitHubBuilder().withOAuthToken(MY_PERSONAL_TOKEN).build();

        //Repository 연결
        GHRepository repo = github.getRepository("whiteship/live-study");

        //IssueState ALL, OPEN, CLOSED
        List<GHIssue> issues = repo.getIssues(GHIssueState.ALL);
        Map<String, Integer> participant = new HashMap<>();

        //1-18개 이슈
        for (GHIssue issue : issues) {
            Set<String> onlyOneParticipant = new HashSet<>();

            //댓글 한개 이상 단 경우 유저이름 중복 제거
            for (GHIssueComment comment : issue.getComments()) {
                onlyOneParticipant.add(comment.getUser().getName());
            }

            //카운트 증가해주기
            for (String name : onlyOneParticipant) {
                if(participant.containsKey(name)){
                    participant.replace(name,participant.get(name)+1);
                    continue;
                }
                participant.put(name,1);
            }
        }
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        //참여율 출력
        for(String name : participant.keySet()){
            double rate = (double)(participant.get(name) * 100) / issues.size();
            bw.write("name : " + name);
            bw.write(", Participation Rate : "+String.format("%.2f",rate)+"%");
            bw.newLine();
        }
        bw.close();
    }

}

 

과제 2. LinkedList를 구현하세요.

  • LinkedList에 대해 공부하세요.
  • 정수를 저장하는 ListNode 클래스를 구현하세요.
  • ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
  • ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
  • boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.

LinkedList Interface

public interface LinkedList {

    // add remove contains
    ListNode add(ListNode head, ListNode nodeToAdd, int position);
    ListNode remove(ListNode head, int positionToRemove);
    boolean contains(ListNode head, ListNode nodeToCheck);
}

ListNode.java

public class ListNode implements LinkedList {
    int data;
    ListNode next;

    public ListNode() {}
    public ListNode(int data) {
        this.data = data;
        this.next = null;
    }

    @Override
    public ListNode add(ListNode head, ListNode nodeToAdd, int position) {
        ListNode node = head;

        //position 이전까지 탐색
        for (int i = 0; i < position - 1; i++) {
            node = node.next;
        }

        //지정 위치에 노드 삽입
        nodeToAdd.next = node.next;
        node.next = nodeToAdd;
        return head;
    }

    @Override
    public ListNode remove(ListNode head, int positionToRemove) {
        ListNode node = head;
				
				//삭제 위치가 가장 앞인경우 
        if(positionToRemove == 0){
            ListNode deleteToNode = node;
            head = node.next;
            deleteToNode.next = null;
            return deleteToNode;
        }
        for (int i = 0; i < positionToRemove - 1; i++) {
            node = node.next;
        }
				
				//지정 위치 노드 삭제
        ListNode deleteToNode = node.next;
        node.next = deleteToNode.next;
        deleteToNode.next = null;
        return deleteToNode;
    }

    @Override
    public boolean contains(ListNode head, ListNode nodeToCheck) {
        while (head.next != null) {
            if (head.next == nodeToCheck)
                return true;
            head = head.next;
        }
        return false;
    }
}

ListNodeTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

class ListNodeTest {
    private ListNode listNode;
    private static final int[] ADD_DATA = {1,3,4,5,7,9};
    private static final boolean[] CONTAINS_DATA = {true, false};
    private static List<Integer> acc_data;

    @BeforeEach
    void setUp() {
        acc_data = new ArrayList<>();
        listNode = new ListNode();
        ListNode firstNode = new ListNode(1);
        ListNode secondNode = new ListNode(3);
        ListNode thirdNode = new ListNode(5);
        ListNode fourthNode = new ListNode(7);
        ListNode fifthNode = new ListNode(9);

        this.listNode = firstNode;
        firstNode.next = secondNode;
        secondNode.next = thirdNode;
        thirdNode.next = fourthNode;
        fourthNode.next = fifthNode;
    }
    @Test
    void add() {
        listNode = listNode.add(listNode,new ListNode(4),2);

        while(listNode != null){
            acc_data.add(listNode.data);
            listNode = listNode.next;
        }

        for(int i=0;i<acc_data.size();i++) {
            Assertions.assertEquals(ADD_DATA[i],acc_data.get(i));
        }

    }

    @Test
    void remove() {
        ListNode removed = listNode.remove(listNode,2);
        Assertions.assertEquals(5,removed.data);
    }

    @Test
    void contains() {
        boolean[] result = new boolean[2];
        result[0] = listNode.contains(listNode,new ListNode(9));
        result[1] = listNode.contains(listNode,new ListNode(10));

        for(int i=0;i<acc_data.size();i++) {
            Assertions.assertEquals(CONTAINS_DATA[i],result[i]);
        }
    }
}

과제 3. Stack을 구현하세요.

  • int 배열을 사용해서 정수를 저장하는 Stack을 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

Stack Interface

public interface Stack {
    void push(int data);
    int pop();
}

ArrayStack.java

public class ArrayStack implements Stack {
    int[] stack;
    int top;

    public ArrayStack(int size) {
        stack = new int[size];
        top = -1;
    }

    @Override
    public void push(int data) {
        stack[++top] = data;
    }

    @Override
    public int pop() {
        if(top == -1){
            System.out.println("Empty");
            return top;
        }
        return stack[top--];
    }
}

ArrayStackTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ArrayStackTest {
    private ArrayStack arrayStack;
    private static final int[] PUSH_DATA = {1,3,5,7,9};

    @BeforeEach
    void setUp() {
        arrayStack = new ArrayStack(5);
    }

    @Test
    void push() {
        arrayStack.push(1);
        arrayStack.push(3);
        arrayStack.push(5);
        arrayStack.push(7);
        arrayStack.push(9);

        for(int i=0;i<arrayStack.stack.length;i++){
            Assertions.assertEquals(PUSH_DATA[i],arrayStack.stack[i]);
        }
    }

    @Test
    void pop() {
        arrayStack.push(1);
        arrayStack.push(3);
        arrayStack.push(5);
        arrayStack.push(7);
        arrayStack.push(9);

        Assertions.assertEquals(9,arrayStack.pop());

    }
}

과제 4. 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.

  • ListNode head를 가지고 있는 ListNodeStack 클래스를 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

ListNodeStack.java

public class ListNodeStack implements Stack {

    static int top;
    ListNode node;
    public ListNodeStack() {
        this.node = null;
        this.top = -1;
    }

    @Override
    public void push(int data) {
        ListNode pushNode = new ListNode(data);
        if(node == null){
            node = new ListNode(data);
            top++;
        }else {
            node = node.add(node, pushNode, ++top);
        }
    }

    @Override
    public int pop() {
        if(top == -1) {
            System.out.println("Empty");
            return top;
        }
        return node.remove(node,top--).data;
    }
}

ListNodeStackTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ListNodeStackTest {
    private ListNodeStack stack;
    private static final int[] PUSH_DATA = {1,3,5,7,9};
    @BeforeEach
    void setUp() {
        stack = new ListNodeStack();
    }

    @Test
    void push() {
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);

        ListNode node = stack.node;

        int i=0;
        while(node != null) {
            Assertions.assertEquals(PUSH_DATA[i++],node.data);
            node = node.next;
        }
    }

    @Test
    void pop() {
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);

        Assertions.assertEquals(9,stack.pop());
        Assertions.assertEquals(7,stack.pop());
        Assertions.assertEquals(5,stack.pop());
        Assertions.assertEquals(3,stack.pop());
        Assertions.assertEquals(1,stack.pop());

    }
}

 

과제 5. Queue를 구현하세요.

  • 배열을 사용해서 한번
  • ListNode를 사용해서 한번.
public class ArrayQueue implements Queueable {
  int[] queue;
  int head, tail;

  public ArrayQueue(int capacity) {
    this.queue = new int[capacity];
    this.head = -1;
    this.tail = 0;
  }

  @Override
  public void push(int data) {
    this.queue[++this.head] = data;
  }

  @Override
  public int pop() {
    if (this.tail > this.head) throw new IndexOutOfBoundsException();
    return this.queue[this.tail++];
  }
}
public class ListNodeQueue implements Queueable {
  ListNode head;

  public ListNodeQueue() {
    this.head = new ListNode();
  }

  @Override
  public void push(int data) {
    ListNode node = new ListNode(data);
    ListNode cur = this.head;
    while (cur.next != null) cur = cur.next;
    cur.next = node;
  }

  @Override
  public int pop() {
    int data = this.head.next.data;
    this.head = this.head.next;
    return data;
  }
}

+ Recent posts