애노테이션과 패키지 — Spring 으로 가는 문
Article
14강에서 @Override 를 붙였다.
@Override
public boolean equals(Object o) { ... }
그런데 이걸 지워도 프로그램은 똑같이 동작한다. 오버라이딩은 그대로 된다.
public boolean equals(Object o) { ... } // @Override 없어도 잘 된다
아무 일도 안 하는 코드를 왜 적을까.
이번 강의에서 그 답을 찾는다. 그리고 그 답은 1강의 첫 질문으로 돌아간다.
실행되지 않는 코드
@Override 는 실행되지 않는다. 바이트코드로 번역되지도 않는다. 표식일 뿐이다.
그런데 14강에서 이런 걸 봤다.
@Override
void descrbe() { } // 오타
- error: method does not override or implement a method from a supertype
누군가 그 표식을 읽고 검사를 했다. javac 다.
이런 표식을 애노테이션이라고 부른다. 자바가 미리 만들어둔 것 몇 개를 이미 만났다.
| 애노테이션 | 누가 읽나 | 무엇을 하나 |
|---|---|---|
@Override | javac | 부모에 그 메서드가 진짜 있는지 검사 (14강) |
@Deprecated | javac | 이건 낡았으니 쓰지 말라고 경고 |
@FunctionalInterface | javac | 추상 메서드가 하나뿐인지 검사 (19강) |
@Test | 테스트 도구 | 이 메서드를 테스트로 실행 |
앞의 셋은 컴파일러가 읽는다. 마지막 @Test 는 다르다. javac 도 JVM 도 @Test 를 모른다.
직접 만들어본다
애노테이션은 @interface 로 정의한다.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test { }
몸통이 비어 있다. 정말 표식일 뿐이다.
붙은 두 애노테이션이 이 표식의 성질을 정한다. 애노테이션에 애노테이션을 붙인 것이다.
@Target(METHOD)— 메서드에만 붙일 수 있다@Retention(RUNTIME)— 실행할 때까지 살아남는다
@Retention 이 중요하다. 세 가지가 있다.
SOURCE
소스 파일까지
javac가 읽고 버린다.class파일에 남지 않는다- 실행 중에는 존재하지 않는다
@Override가 이것이다
RUNTIME
실행할 때까지
.class파일에 기록된다- 실행 중에 읽을 수 있다
- 누군가 찾아 읽어야 의미가 생긴다
@Test가 이것이다
확인해보자. 두 표식을 클래스 하나에 붙이고, 실행 중에 무엇이 보이는지 본다.
@Retention(RetentionPolicy.SOURCE) @interface Src { }
@Retention(RetentionPolicy.RUNTIME) @interface Run { }
@Src @Run class Marked { }
for (Annotation a : Marked.class.getAnnotations()) {
System.out.println(a);
}
- @Run()
@Src 는 없다. javac 가 읽고 버려서 .class 에 남지 않았다.
1강에서 .class 파일이 "JVM 이 읽으라고 만든 글자"라고 했다. RUNTIME 애노테이션은 그 파일에 실행 코드가 아닌 메모로 함께 적힌다.
그 메모를 누가 읽는가
@Test 를 붙인 메서드만 골라 실행하는 프로그램을 만들어보자.
class MyTests {
@Test void first() { System.out.println("first 실행됨"); }
@Test void second() { System.out.println("second 실행됨"); }
void notATest() { System.out.println("이건 안 불려야 한다"); }
}
for (Method m : MyTests.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
System.out.println("찾았다: " + m.getName());
m.invoke(new MyTests());
}
}
- 찾았다: first
- first 실행됨
- 찾았다: second
- second 실행됨
notATest 는 불리지 않았다.
이 코드가 하는 일을 보자. 클래스를 열어서 메서드 목록을 훑고, 표식이 붙은 것을 골라, 이름도 모르는 채로 실행했다. 우리는 first() 라고 직접 쓴 적이 없다.
이런 능력을 리플렉션이라고 부른다. 프로그램이 자기 자신의 구조를 실행 중에 들여다보는 것이다.
- 우리가 표식을 붙인다@Test void first()
- javac 가 .class 에 적어둔다RUNTIME 이니까
- JVM 은 그냥 무시한다실행할 코드가 아니다
- 누군가 찾아 읽는다그제서야 뜻이 생긴다
1강의 질문
1강에서 물었다. 자바 코드는 대체 누가 실행하는가.
답은 이랬다. javac 가 바이트코드로 번역하고, JVM 이 그 바이트코드를 읽어 실행한다.
이제 한 겹이 더 생겼다.
- Hello.java
- javac 가 읽는다
- Hello.class
- JVM 이 읽는다
- 프레임워크가 읽는다
- javac 가 읽는다
바이트코드는 JVM 이 읽고, 애노테이션은 JVM 이 아니라 프레임워크가 읽는다.
@Test 는 자바 문법이 아니다. 테스트 도구가 만든 표식이고, 그 도구가 리플렉션으로 찾아 읽는다. 표식 자체에는 아무 힘이 없다. 읽는 사람이 힘을 준다.
패키지 — 클래스에 성을 붙인다
클래스가 많아지면 이름이 겹친다. Book 이라는 클래스를 두 사람이 만들면 충돌한다.
폴더로 나누고, 파일 맨 위에 소속을 적는다.
package shop;
public class Cart { ... }
이 클래스의 정식 이름은 shop.Cart 다. 다른 폴더의 Cart 와 겹치지 않는다.
다른 패키지의 클래스를 쓰려면 import 로 가져온다.
import shop.Cart;
9강에서 import java.util.Arrays; 를 적었던 그것이다. java.lang 패키지만은 적지 않아도 된다. String, System, Object, Integer 가 전부 거기 있다. 그래서 1강부터 System.out.println 을 그냥 썼다.
패키지 이름은 회사 도메인을 거꾸로 쓰는 관례가 있다. com.dcodelab.shop 같은 식이다. 세상 누구와도 안 겹치게 하려는 것이다.
JAR — 하나로 묶는다
.class 파일이 수백 개가 되면 배포하기 곤란하다. 압축해서 하나로 묶는다.
jar cf myapp.jar .
java -cp myapp.jar shop.Main
.jar 파일은 그냥 압축 파일이다. 확장자를 .zip 으로 바꾸면 열린다. 안에는 .class 파일들이 폴더 구조 그대로 들어 있다.
우리가 쓰는 라이브러리도 전부 .jar 다. 남이 컴파일해둔 .class 묶음을 가져다 쓰는 것이다.
21강을 마치며
여기까지다. 스물한 강의를 지나왔다.
처음에 java Hello 가 클래스를 못 찾겠다고 거절했다. 지금은 그 이유를 안다. 그리고 그 사이에 네 갈래가 강의를 관통하며 흘렀다.
코드를 누가 읽는가
1강에서 물었다. javac 가 읽고 JVM 이 읽는다고 답했다. 2강에서 JVM 이 main 이라는 이름을 찾는 것을 봤고, 13강에서 그 main 이 static 이라서 객체 없이 불릴 수 있다는 걸 알았다. 그리고 방금, 애노테이션은 그 둘이 아니라 제3자가 읽는다는 것을 봤다.
== 는 주소를 본다
4강에서 기본형은 값을, 참조형은 주소를 담는다고 했다. 6강에서 그래서 String 을 == 로 비교하면 안 된다고 했다. 14강에서 .equals() 의 정체가 Object 에서 물려받은 메서드였다는 게 밝혀졌고, 16강에서 hashCode 를 함께 고치지 않으면 HashMap 이 방금 넣은 값을 잃어버리는 것을 봤다.
선언한 타입과 실제 타입은 다르다
4강에서 상자와 내용물을 나눴다. 14강에서 Product p = new Ebook(...) 이 Ebook 의 메서드를 부르는 것을 봤다. 15강에서 그 성질로 구현체를 바꿔 끼웠다. 17강에서 꺾쇠가 컴파일러에게만 보이고 실행 중에는 사라진다는 것을 알았다.
null 은 값이 아니다
4강에서 참조형의 기본값으로 처음 만났다. 16강에서 map.get() 이 조용히 돌려주는 것을 봤다. 18강에서 붙잡는 법을 배웠고, 20강에서 애초에 없을 수 있다는 사실을 타입에 적는 법을 배웠다.
이 네 갈래는 자바를 쓰는 내내 돌아온다. 문법은 잊어도 좋다. 필요할 때 찾으면 된다. 하지만 이 네 가지는 코드를 읽을 때마다 머릿속에서 작동해야 한다.
그리고 다음
이 시리즈는 System.out.println 하나로 시작해서 끝까지 그것만 썼다. 화면도, 웹도, 데이터베이스도 없었다.
이제 그 다음이다. 방금 배운 것을 마지막으로 한 번만 더 써보자.
@RestController
class BookController { ... }
이 표식이 무엇을 하는지 우리는 아직 모른다. 하지만 무엇을 하지 않는지는 안다. 저것은 실행되지 않는다. 아무 일도 하지 않는다.
누군가 리플렉션으로 저 표식을 찾아 읽고, 그 클래스를 대신 만들어서, 어딘가에 연결해줄 것이다. 그 누군가의 이름이 Spring 이다.
다음 시리즈에서 만난다. 거기서 우리는 new 를 거의 쓰지 않게 된다. 15강에서 OrderService 가 Notifier 를 스스로 만들지 않고 생성자로 받았던 것을 기억하는지. 그게 전부다. 나머지는 그 위에 얹힌 도구일 뿐이다.
고생했다. 스물한 강의를 끝까지 읽었다면, 자바 코드를 읽고 쓰는 데 필요한 것은 이미 다 갖췄다. 모르는 클래스 이름이 나와도 괜찮다. 그건 찾아보면 되는 것이고, 지금 갖춘 것은 찾아본 내용을 이해할 수 있는 바탕이다.
읽어줘서 고맙다.
정리하면
애노테이션은 아무 일도 하지 않는 표식이고, 누군가 읽어야 뜻이 생긴다.
- 01
애노테이션은 실행되지 않는다
@Override 를 지워도 프로그램은 똑같이 동작한다. 검사만 사라진다.
- 02
@Retention이 수명을 정한다SOURCE 는 javac 가 읽고 버리고, RUNTIME 은 .class 에 남아 실행 중에도 읽힌다.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Test { } - 03
리플렉션이 그 표식을 읽는다
클래스를 열어 메서드를 훑고, 표식이 붙은 것을 골라 실행한다.
if (m.isAnnotationPresent(Test.class)) m.invoke(obj); - 04
패키지는 클래스의 성이다
java.lang 만 import 없이 쓸 수 있다. 그래서 String 과 System 을 그냥 썼다.
- 05
바이트코드는 JVM 이, 애노테이션은 프레임워크가 읽는다
1강의 질문에 대한 마지막 답이다.