코드스쿼드 미션을 진행하던 중 사용자로부터 콘솔 입력을 받아서 값을 반환해주는 메서드를 테스트 하는 방법을 찾아보니 콘솔 입력을 값으로 넣어줄 수 있는 방법이 있었다.

 

내가 테스트한 메서드는 inputLadderHeight()로 필드 변수인 scanner를 통해 입력을 받고 있다. (원래는 scanner를 static final로 선언하고 메서드도 static으로 작성했는데 변경된 상태이다.) 

 

메서드를 보면 내부에서 scanner.nextLine()으로 입력을 읽는데 Scanner를 생성할때 java.lang 패키지의 System.in을 파라미터로 전달을 한다. 설명을 보면 이 표준 InputStream은 이미 열려서 데이터를 받을 준비가 되어있고, user가 설정할 수도 있다고 나와 있다.

 

스레드 초기 설정 후에 initPhase1() 메서드에서 System class를 초기화 하는데 처음에 Systen.in이 java.io 패키지의 BufferedInputStream으로 초기화가 되는 것을 확인할 수 있다.

 

 

다시 맨 위 코드를 보면 Screen 클래스가 생성될때 Scanner도 초기화가 되는데 이때 System의 static final 변수인 in이 전달이 된다.

그래서 Screen을 생성하기 전에 System.setIn()으로 테스트용 InputStream을 set 해주면 내가 임의로 만든 테스트 입력 값을 Scanner가 읽도록 할 수 있다.

 

 

테스트 코드를 보면 InputStream을 만들어서 Scanner가 초기화 되기 전에 System.in을 교체해주면 된다.

 

 

처음에 Scanner를 static final로 선언했더니 여러번 반복 테스트를 할 때 계속 의도한 대로 동작하지가 않았다.

생각을 해보니 여러번 테스트를 할 때 InputStream을 새로 생성해서 setIn()을 해줘도 처음 Screen 클래스가 로드되면서 Scanner에 주입된 InputStream이 그대로 있어서 입력이 들어가지 않았던 것이다.

 

역시 static은 되도록 사용하지 않는 것이 좋다고 느꼈다..

 

 

 

[참고]

https://sakjung.tistory.com/33

https://www.geeksforgeeks.org/java-lang-system-class-java/

https://mommoo.tistory.com/71

static import를 사용하면 static 메서드를 사용할 때나 이너 클래스를 사용할 때 클래스명을 생략할 수 있다. 잘 활용하면 테스트 코드를 작성할때 가독성을 올려준다거나 하는 이점도 있지만 패키지 구조를 다 아는 내 눈에만 예뻐 보일 수 있겠다는 생각이 들었다.

 

오히려 가독성을 떨어트리고 헷갈리게 할 수 있기 때문에 전역적으로 사용되고 static import를 해도 이해하기 쉬운 부분에 사용하는 것이 좋을 것 같다.

 

 

언제 사용하면 좋은지 해당 글에서 보았다.

 

Static Import

In order to access static members, it is necessary to qualify references with the class they came from. For example, one must say: double r = Math.cos(Math.PI * theta); In order to get around this, people sometimes put static members into an interface and

docs.oracle.com

 

when should you use static import? Very sparingly! 

use it when you require frequent access to static members from one or two classes.

 

아주 드물게 사용해야 한다고 강조하고 있다!

 

If you overuse the static import feature, it can make your program unreadable and unmaintainable, polluting its namespace with all the static members you import. Readers of your code (including you, a few months after you wrote it) will not know which class a static member comes from. Importing all of the static members from a class can be particularly harmful to readability

대충 남용하면 가독성 떨어지고.. 유지 보수 어렵고.. 네임스페이스 오염시키고.. 코드 읽는 사람이 헷갈리고.. 등등 엄청 안좋다는 말

 

if you need only one or two members, import them individually. Used appropriately, static import can make your program more readable, by removing the boilerplate of repetition of class names.

 

이전에 혼자 코드 짤 때 static import 하는게 깔끔한거 같아서 막 다 static import를 했었는데 다른 분이 짠 코드를 보다보니 static import가 안 되어있는 것이 보기에 이해가 더 잘 됐다. 그래서 이거 사용하는거 맞나 의문이 들었는데 찾아보길 잘한 것 같다.

 

 

 

 

[참고]

 

[Java] Static import에 대한 관찰

JDK5에서 Static import가 추가되었다. 먼저 static import를 적용하지 않은 일반 코드를 보자. 가장 기본적인 용법은 import문 뒤에 static을 붙이고, {패키지.클래스.\*} 혹은 {패키지.클래스.멤버} 를 적으면

velog.io

 

제네릭메서드나 클래스에 컴파일 시 타입 체크를 해주는 기능으로 컴파일 단계에서 타입 체크를 하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

 

타입 안정성을 높인다는 것은 의도치 않은 타입의 객체가 저장되는 것을 막고, 형변환의 오류를 줄여주는 것이다.

 

 

class Box <T> {
    T item;
    
    void setItem(T item) {
        this.item = item;
    }
    
    T getItem() {
        return item;
    }
}

T 를 타입 변수(타입 매개변수)라고 하는데 여기서 T는 'Type'의 T를 뜻하며, 요소의 경우 'Element'의 첫 글자인 'E'로 표현할 수도 있다. 좌표를 보통 x,y로 나타내는 것처럼 꼭 T, E, K 이런게 정해져 있는것은 아니지만 코드를 이해하는데 있어 맞추는 것이 중요하다.

기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 같다.

 

제네릭에서 타입 변수를 사용하지 않는 경우에도 문제가 생기진 않는데 이는 호환성으로 인해 가능하지만 주의하는 것이 좋다고 한다.

 

 

[Java] 제네릭에서 Raw Type 선언 주의 (이펙티브 자바)

'Raw use of parameterized class 'List' 라는 경고를 보고 Raw use가 무엇인지 찾아봤는데 이펙티브 자바에 있는 내용이라 아직 초반부를 보고 있지만 해당 부분을 읽어보았다. 제네릭 타입은 일련의 매개변

treecode.tistory.com

 

Box <String>으로 선언을 하면 타입 매개변수에 String이 입력이 돼서 컴파일 후에 원시 타입인 Box로 바뀌게 되고 제네릭 타입은 제거되며 타입 매개변수의 'T'가 String으로 치환이 되고 적절하게 형변환이 된다. 

 

class FrutBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<T>();
    ...
}

 

 그냥 T 만 사용하면 모든 타입이 들어올 수 있는데 위와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한할 수 있다. (인터페이스의 경우에도 extends)

 

 

제네릭과 와일드 카드 <?>

 

제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하기 때문에 제네릭 타입을 다르게 하는 것으로는 오버로딩이 성립하지 않는다. 이럴 때는 와일드 카드 '?'를 사용하면 되는데 다음과 같이 사용한다.

 

1) <? extends T> 상한 제한, T와 그 자손들만 가능

2) <? super T> 하한 제한, T와 그 조상들만 가능

3) <?> 모든 타입 가능

 

제네릭 타입을 클래스 말고 메서드에 선언한 것을 제네릭 메서드라고 하는데 이번에 글로 정리하게 된 이유이다.

 

 

제네릭 메서드

 

제네릭 메서드는 말 그대로 메서드에 제네릭을 선언하는 것으로 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 서로 별개의 것인데 제네릭 메서드는 제네릭 클래스가 아니더라도 정의할 수 있다.

 

제네릭 메서드에 선언된 타입 매개변수는 해당 메서드 내에서만 지역적으로 사용이 되기 때문에 지역 변수를 선언한 것과 같다고 생각해도 된다. (타입 매개변수는 인스턴스 변수로 간주되기 때문에 원래 static 멤버에는 사용할 수 없지만, 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다.)

(-> 그렇구나.. 하긴 했지만 아직 정확히 이해는 안됨)

 

static <T extends Fruit> Juice makeJuice(FruitBox<T> box)

 

제네릭 메서드를 호출할 때는 메서드 앞에 타입을 대입해야 하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.

 

Juicer.<Fruit>makeJuice(fruitBox);

 

책에 밑줄이 그어져 있는 것으로 보아 이전에도 보긴 했었는데 확실히 보기만 하고 실제로 사용을 안하니 금방 까먹고 이해도 잘 안되는 것 같다.  공부하고 있는 프로젝트의 코드를 다시 보았다.

 

 

해당 메서드는 Api 응답을 만드는 메서드로 매개변수에 어떤 클래스가 들어올 지 몰라서 매개변수 타입을 제네릭 타입 변수 'T'로 선언을 해야 한다. 원래 static 메서드에는 타입 변수 'T' 를 선언할 수 없지만 제네릭 메서드로 선언을 하면 매개변수에 타입 변수를 사용할 수 있다.

 

처음에 T가 3개씩 보여서 제네릭 모른다고 당황했었는데 이렇게 다시 보니 이해가 좀 된다.

 

 

문제의 코드인데 ApiResult를 builder로 생성해서 반환하니 incompatible types 에러가 떠서 어리둥절 했었다. 

 

롬복의 builder()도 제네릭 메서드로 선언이 되어 있는데 제네릭 메서드를 호출할 때는 원래 메서드 앞에 타입 변수를 대입해야하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략을 하고 사용한다. 하지만 아래와 같이 제네릭 클래스에서 builder()를 사용하는 경우 타입 변수를 생략하면 Object로 반환이 되어서 명시적으로 타입 선언을 해줘야 한다.

 

 

 

 

[참고]

자바의 정석

 

자바 제네릭(Generics) 기초

tecoble.techcourse.co.kr

 

면접에서 인터페이스에 대해 물어보는 질문에 당연히 알고 있다고 생각했는데 막상 말하려고 하니 제대로 떠오르지가 않아서 아쉬움이 있었다. 앞으로 공부를 할 때는 이걸 왜 사용하는지 어떤 이점이 있는지 이런 부분을 좀 더 생각하고 정리해야겠다.

 

먼저 인터페이스를 알기 위해서는 추상 클래스에 대해 알아야 하는데 추상 클래스(abstract class)란 미완성 클래스로 미완성 상태의 추상 메서드를 포함하고 있는 것이다. 추상 메서드는 abstract 키워드를 사용하며 구현 코드 없이 선언부만 존재한다. 추상적인 클래스(설계도)이기 때문에 자체로 인스턴스를 생성할 수 없고 이를 상속 받은 자식 클래스는 반드시 추상 메서드를 구현해야한다. 

 

추상 메서드를 선언하는 이유는 자식 클래스에게 구현을 강요하기 위함으로 추상 메서드를 구현하지 않으면 에러가 발생하기 때문에 실수를 방지할 수 있고 추상화를 통해 확장성을 넓힐 수 있다. 그리고 추상 클래스를 상속 받은 자식 클래스를 부모 타입의 참조 변수로 참조하면 여러 자식 클래스를 하나의 배열로 다룰 수도 있다.

 

인터페이스란?

 

인터페이스는 일종의 추상 클래스로 추상 클래스보다 추상화 정도가 더 높으며 추상 메서드와 상수로 이루어져 있다. (JDK 1.8부터는 인터페이스에 추상메서드 외 static, default 메서드도 선언이 가능하다.)

 

인터페이스는 다음과 같은 제약이 있다.

 

1. 멤버 변수는 public static final이어야 하며 생략이 가능하다.

2. 메서드는 public abstract이어야 하며, 생략이 가능하다. 

 

인터페이스는 왜 사용하는가?

 

인터페이스를 사용하면 클래스 사이(객체 사이)에 새로운 관계를 맺어 줄 수 있고 다형성을 통해 의존관계를 추상화하여 런타임 시점에 외부에서 객체를 생성하고 주입할 수 있다. 이로 인해 클래스 간에 결합도를 낮출 수 있고 객체 지향 프로그래밍의 설계 원칙인 SOLID 5원칙 중 단일 책임 원칙, 개방 폐쇄 원칙, 의존관계 역전 원칙을 지킬 수 있다.

 

- 단일 책임 원칙(SRP, 하나의 클래스는 하나의 책임만 가진다)

- 개방 폐쇄 원칙(OCP, 변경에는 닫혀있고 확장에는 열려 있어야 한다)

- 의존관계 역전 원칙(DIP, 구체화가 아닌 추상화에 의존해야한다)

 

 

 

 

[참고]

자바의 정석

이것이 자바다

https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

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

+ Recent posts