Raw types should not be used

급식·2023년 7월 9일
1
post-thumbnail

어쩌다 발견했나

요즘 소마 프로젝트 진행을 위해 Spring boot 공부를 하고 있다. 이때까지 Python-Django, Typescript-Node로 서버 프로젝트를 진행해왔는데, Python이야 duck typing 덕분에 크게 이것저것 신경쓰지 않아도 코드가 써지긴 써졌고, typescript에서는 인터페이스의 중요성을 약간이나마 느낄 수 있었지만 돌아보고 나면 항상 코드에서 구린내가 진동을 했다. 🤢

그래서 요즘 객체 지향 설계 원칙이니, SOLID니, 이런 저런 코드의 구조적인 공부도 하는 한편 내가 맡지 못한 구린내들도 잡아보고 싶다는 생각이 들었다. 아무래도 혼자 힘으로 개선하기는 어려울 것 같아 아래 IntelliJ 플러그인의 힘을 빌리고 있다.

  • CodeMetrics: 프로젝트에서는 클래스 내의 메서드 갯수나 반환문, 예외 처리 등 이런 저런 코드의 복잡성을 계산해주는 플러그인이다. 기회가 된다면 따로 다루어볼 예정!
  • SonarLint: 여러 정적인 기준에 의하여, 코드에 숨어 있는 버그, 취약점, 구린내를 감지해서 알려주는 플러그인이다.

제목부터 그렇지만 플러그인에 대한 이야기를 하자는건 아니고, SonarLint가 잡아준 이슈 중 다른건 끄덕끄덕 하면서 고쳤지만 '이게 왜,,?' 싶은 문제가 딱 하나 남아 있길래 각잡고 분석, 개선해보려 한다.


문제의 발견

이제 쳐낼거 다 쳐냈다고 생각했는데, 구석에 이런 경고 문구가 있었다.

Provide the parameterized type for this generic.
= 이 제네릭에 매개변수화된 타입을 넘겨 주어야 한다.

뭔 소리인지,, 자세히 들여다 보면

"Raw types should not be used", 직역하자면 '날것의 타입을 쓰면 안된다' 정도일 것 같다. 'Code Smell', 'Major' 씩이나 붙어 있는걸 보니 자세히 봐야될 것 같은데 설명부터 읽어보면,, 니 코드 더러워!

Generic type shouldn't be used raw (without type parameters) in variable declarations of return values.
= 제네릭 타입은 반환값의 변수 선언에서 날것으로(타입 매개 변수 없이) 사용되면 안된다.

Doing so bypasses generic type checking, and defers the catch of unsafe code to runtime.
= 이렇게 하면 제네릭 타입 검사를 우회하고 안전하지 않은 코드의 포착을 런타임으로 연기한다.

Noncompliant/Compliant로 구분되어 나와 있으니, noncompliant한 내 코드를 compliant하게 바꿔주면 되겠지? 김영한님의 스프링 강의를 듣다가 나온 코드라 자세히는 못 쓰고, 맥락상 의미만 맞게 옮기겠다.


해결

private static final Map<Long, MyClass> myMap = new HashMap();

이걸

private static final Map<Long, MyClass> myMap = new HashMap<>();

이렇게 바꿔주면 된다. 척 보기에는 큰 차이가 없어보이는데, 이유가 뭘까?

Generic?

일단, 자바에서 제네릭은 왜 쓰는 걸까?

TCP school에 의하면, JDK 1.5 전까지는 정해지지 않은 타입을 파라미터로 써야 할 때 모든 객체의 조상 클래스인 Object 클래스로 타입을 정해 코드 안에서 내가 의도한 타입으로 변환하도록 작성해주었다고 한다. 이거 완전 Typescript에서 any 남발하는 그런 느낌인데,,?

여하튼 이 타입 변환을 제대로 안해주면 당연히 오류가 뿜어 나왔기 때문에, JDK 1.5에서는 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 정하는 Generic이 도입되었다고 한다. 또 이건 몰랐는데, 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해 컴파일될 때 제네릭 타입은 싹 변환되어 제거된다고 한다. 🔑

Raw type?

혼자 생각해봐도 답이 안나오길래 이것저것 찾아보다가 알아낸건데, 위에서 '날것'으로 써놓은게 실은 raw type이라는 용어에서 나온 거란다. 예를 들어 List<E>의 raw type은 List인데, 보다시피 제네릭 타입에서 타입 매개 변수(E)를 사용하고 있지 않은 것을 의미한다. 위에서 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해 컴파일될 때 제네릭 타입은 모두 제거된다고 했는데, 이런 raw type을 쓰면 제네릭으로 써놔도 이 제네릭 타입 정보가 지워진 것처럼 작동한다는 의미였다.

예를 들어 MyCollection<T extends Number>로 정의해봤자, new MyCollection() 으로 써버리면 기껏 써넣은 제네릭 타입 정보가 날아갔으니 String이고 뭐고 다 들어갈 수 있게 될 것이다. 당연히 의도한 상황이 아니었으니 버그가 발생하겠지?

따라서 내 코드에서 new HashMap()new HashMap<>()으로 바꿔서 경고가 사라진건, raw type이 제거되었기 때문이다. 꺽쇠 안이 비어 있는데 제대로 작동하는건 변수의 타입을 선언하는 부분에 기반한 타입 추론 덕분이고!

그럼,,

private static final Map mayMap = new HashMap();

이렇게 쓰면 당연히 타입 선언, 생성자 호출 두 부분에서 경고가 뜬다. 에러 말고 경고.

public MyClass findByKey(Long someLong) {
    return store.get(someLong);
}

에러는 이 부분에서 떴다. findByKey의 반환형이 MyClass로 되어 있는데, store.get(someLong)의 반환값이 Object라서 문제라고 하는데, 저렇게 raw type으로 써주면 타입이 냅다 Key든 Value든 타입이 냅다 Object로 변환되어서 그렇다. 아하!


여담으로

이 내용은 Effective Java에 수록된 내용이라고 한다. 바로 공부해서 바로 써먹을 수 있는 것들만 읽었지, 돌아보면 java라는 언어를 java 답게 쓸만큼 잘 알고 있지는 않았던 것 같다. 나중에 읽어봐야지. 반성,,

그리고 이 내용에 대해 엄청 잘 정리해두신 분의 글이 있었는데, 나중에 다시 한 번 읽어보러 가야겠다.

profile
뭐 먹고 살지.

1개의 댓글

comment-user-thumbnail
2023년 7월 17일

멋져요~

답글 달기