어노테이션이란?
- Annotation이라는 단어는 일반적으로 주석 또는 메모를 뜻하지만 자바에서는 주석 외에 추가 정보 제공 역할도 함.
- 코드에게 메타데이터를 추가해주는 기능
- 리플렉션은 메타데이터를 동적으로 조회 및 조작하는 기능을 제공하므로, 리플렉션을 통해 어노테이션을 활용 가능
어노테이션에 정의된 값은 어떻게 가져올 수 있는가?
@AnnoElement(value = "data", count = 10, tags = {"t1", "t2"})
public class ElementData1 {
}
public static void main(String[] args) {
Class<ElementData1> annoClass = ElementData1.class;
AnnoElement annotation = annoClass.getAnnotation(AnnoElement.class);
String value = annotation.value();
System.out.println("value = " + value);
...
- 위와 같이 리플렉션을 통해 클래스정보를 가져옴
- 가져온 클래스 정보에서 어노테이션을 가져와 내부의 값을 조회할 수 있음.
메타 어노테이션 Rentention, Target, Documented, Inherited에 대해 설명하시오
- 어노테이션을 정의하는데 사용하는 특별한 어노테이션을 메타 어노테이션이라 함.
- @Retention
- 어노테이션의 생존 기간을 의미함
- RetentionPolicy.SOURCE : 소스 코드에만 남아있다. 컴파일 시점에 제거된다.
- RetentionPolicy.CLASS : 컴파일 후 class 파일까지는 남아있지만 자바 실행 시점에 제거된다. (기본 값)
- RetentionPolicy.RUNTIME : 자바 실행 중에도 남아있다. 대부분 이 설정을 사용한다.
- @Target
- 어노테이션을 적용할 수 있는 위치를 지정함
- 다음과 같은 타입을 지정할 수 있으며 일반적으로 TYPE, FIELD, METHOD를 사용함
- TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, MODULE, RECORD_COMPONENT;
- @Documented
- 자바 API 문서를 만들 때 해당 어노테이션이 함께 포함될 지를 지정하며 보통 함께 사용함
- @Inherited
- 자식 클래스가 어노테이션을 상속 받을 수 있게 함.
모든 어노테이션이 확장하는 인터페이스는?
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
- 위와 같은 Annotation 인터페이스를 모든 어노테이션은 자동으로 확장함
- @interface 키워드를 통해 정의하면 자동으로 확장하도록 함.
- 어노테이션은 다른 어노테이션을 확장하는 기능은 없고 오직 Annotation 인터페이스만 확장함.
@Inherited의 상속에 대해 자세히 설명하시오
- 해당 어노테이션을 적용한 클래스의 자식도 해당 어노테이션을 부여받도록 함
- 이는 클래스 상속에서만 작동하며, 인터페이스의 구현체에는 적용되지 않음.
- 상속은 자식이 부모의 특성을 이어받는 개념이기 때문에 적용됨
- 하지만, 인터페이스는 메소드의 시그니처만 정의할 뿐 어떤 상태나 행위를 갖지 않기에 구현체가 어노테이션을 상속한다는 개념이 맞지 않은 것.
- 또한, 인터페이스는 다중 구현이 가능하므로 이 경우 충돌이 발생할 수도 있음.
특정 필드가 Null 일 경우 오류를 내도록 하는 어노테이션을 구현하시오
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
String message() default "값이 비어있습니다.";
}
public static void validate(Object obj) throws Exception {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(NotEmpty.class)) {
String value = (String) field.get(obj);
NotEmpty annotation = field.getAnnotation(NotEmpty.class);
if (value == null || value.isEmpty()) {
throw new RuntimeException(annotation.message());
}
}
...
- @Target을 FIELD로 지정
- @Rentention은 RUNTIME으로 지정해야 코드 실행시 적용 가능
- 특정 객체의 필드들을 가져와 setAccessible을 true로 만들어 private도 접근가능하도록 설정
- 해당 필드가 NonEmpty 어노테이션을 갖고 있는지 확인하고 만약 맞다면 null 체크를 하고 NonEmpty의 메시지를 출력하도록 함.
- 이 어노테이션을 null 체크가 필요한 필드에 모두 입력한 후 null 체크 필요할 때마다 validate 함수를 호출.
- 이는 클래스나 필드가 달라도 모두 적용 가능함.
- 새로 추가되는 클래스나 필드에도 적절히 확장 가능함.
- 코드 가독성에도 좋음
자바 기본 어노테이션 Override, Deprecated, SuppressWarning에 대해 설명하시오
- @Override
- 매서드 재정의가 정확하게 잘 되었는지 컴파일러가 체크하는데 사용
- 컴파일러단에서 체크하기 때문에 실수 방지를 위해서라도 재정의할때는 사용하는게 좋음
- RententionPolicy가 SOURCE이므로 컴파일 이후 어노테이션은 제거됨
- @Deprecated
- 더 이상 사용되지 않는다는 의미로, 이 어노테이션이 적용된 기능은 사용을 권장하지 않음.
- 해당 요소를 사용하면 오류 발생 가능성이 있거나 향후 변경 또는 제거될 가능성이 있음
- 혹은, 더 나은 대체제로 대체되었거나 현재 더이상 사용중이지 않다는 것을 의미할 수도.
- @Deprecated(since = "2.4", forRemoval = true)
- 2.4 버전 이후 더 이상 사용하지 않게 되었고 미래 버전에 코드가 제거될 예정을 의미
- Deprecated만 있는 경우 경고를 IDE가 보여주며, forRemoval true 일 경우 심각한 경고를 의미
- SuppressWarnings
- 경고를 억제하는 어노테이션으로, 자바 컴파일러가 문제를 경고하지만 개발자가 문제에 대해 잘 알기에 더는 경고하지 말라고 지시하는 어노테이션
- ex) @SuppressWarnings({"rawtypes", "unchecked"})
- all: 모든 경고를 억제
- deprecation: 사용이 권장되지 않는(deprecated) 코드를 사용할 때 발생하는 경고를 억제
- unchecked: 제네릭 타입과 관련된
- unchecked 경고를 억제
- serial: Serializable 인터페이스를 구현할 때 serialVersionUID 필드를 선언하지 않은 경우 발생하는 경고를 억제
- rawtypes: 제네릭 타입이 명시되지 않은(raw) 타입을 사용할 때 발생하는 경고를 억제
- unused: 사용되지 않는 변수, 메서드, 필드 등을 선언했을 때 발생하는 경고를 억제
어노테이션을 활용하여 특정 URI Path가 요청으로 들어오면 해당 요청에 맵핑되는 함수를 호출되도록 하는 코드를 작성하시오
public class AnnotationController {
@Mapping("/")
public void home(HttpRequest request, HttpResponse response) {
}
@Mapping("/site1")
public void site1(HttpRequest request, HttpResponse response) {
}
@Mapping("/site2")
public void site2(HttpRequest request, HttpResponse response) {
}
}
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mapping.class)) {
Mapping mapping = method.getAnnotation(Mapping.class);
String value = mapping.value();
if (value.equals(path)) {
invoke(controller, method, request, response);
return;
}
}
}
}
throw new PageNotFoundException("request=" + path);
}
- 컨트롤러의 클래스 정보에서 모든 메소드를 불러옴
- 메소드가 Mapping이라는 어노테이션을 가지고 있는지 확인하고, 해당 어노테이션의 value가 요청 들어온 path와 가은지 확인
- 만약 같으면 해당 함수를 호출하는 메소드 실행
위 코드의 문제점을 제시하고, 개선할 수 있는 방안을 제시하시오.
- 모든 메소드를 뒤져야 하므로 O(n) 번의 복잡도를 갖고 있어 성능 문제가 있음.
- 똑같은 value를 가지고 있는 Mapping 어노테이션이 있을 수 있는 문제가 있음.
- 따라서, HashMap을 통해 미리 uri와 함수를 맵핑하는 맵을 만들고 성능을 개선하고 중복이 발생할 경우 예외를 던지는 기능을 추가하며 됨.
- Map을 만드는 기능은 생성자에 만들어서 처음에 한 번만 실행되도록 함.
public class AnnotationServletV3 implements HttpServlet {
private final Map<String, ControllerMethod> pathMap;
public AnnotationServletV3(List<Object> controllers) {
this.pathMap = new HashMap<>();
initializePathMap(controllers);
}
private void initializePathMap(List<Object> controllers) {
for (Object controller : controllers) {
for (Method method : controller.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(Mapping.class)) {
String path = method.getAnnotation(Mapping.class).value();
// 중복 경로 체크
if (pathMap.containsKey(path)) {
ControllerMethod controllerMethod = pathMap.get(path);
throw new IllegalArgumentException("경로 중복 등록, path="
+ path + ", method=" + method + ", 이미 등록된 메서드=" + controllerMethod.method);
}
pathMap.put(path, new ControllerMethod(controller, method));
}
}
}
}
}