elevne's Study Note

Java (Singleton, 접근제어, Annotation) 본문

Backend/Java

Java (Singleton, 접근제어, Annotation)

elevne 2023. 4. 26. 23:02

간혹 전체 프로그램 내에서 단 하나의 객체만 만들도록 보장해야 하는 경우들이 있다. 이를 Singleton 이라고 부른다. 이를 구현하기 위해서는, 클래스 외부에서 new 연산자로 생성자를 호출할 수 없도록 막아야 한다. 생성자를 외부에서 호출할 수 없도록 private 접근 제한자를 붙여주면 된다. 대신, 외부에서 호출할 수 있는 static 메서드를 하나 선언해주면 된다. 아래의 getInstance 메서드는 단 하나의 객체만 리턴하게 된다.

 

 

 

package test_classes2;

public class SingletonTest {
    
    private static SingletonTest singleton = new SingletonTest();
    
    private SingletonTest() {}
    
    static SingletonTest getInstance() {
        return singleton;
    }
    
}

 

 

 

 

main() 메서드를 가지지 않는 대부분의 클래스는 외부 클래스에서 이용할 목적으로 설계되는 라이브러리 클래스이다. 이들을 설계할 때는 외부 클래스에서 접근할 수 있는 멤버와 접근할 수 없는 멤버로 구분해서 필드, 생성자, 메서드를 설계하는 것이 바람직하다. 

 

 

 

접근제한 적용대상 접근할 수 없는 클래스
public 클래스, 필드, 생성자, 메서드 없음
protected 필드, 생성자, 메서드 자식 클래스가 아닌 다른 패키지에 소속된 클래스
default 클래스, 필드, 생성자, 메서드 다른 패키지에 소속된 클래스
private 필드, 생성자, 메서드 모든 외부 클래스

 

 

 

일반적으로 객체 지향 프로그래밍에서, 객체의 무결성이 깨어질 것을 염려하여 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 막는다. 객체 지향 프로그래밍에서는 메서드를 통해서 데이터를 변경하는 방법을 선호한다. 데이터는 외부에서 접근할 수 없도록 막고 메서드는 공개해서 외부에서 메서드를 통해 데이터에 접근하도록 유도하는 것이다. Getter, Setter 을 사용한다. 

 

 

 

 

그 다음으로는 Annotation 에 대해서 알아본다. Annotation 은 metadata 라고 볼 수 있다. metadata 란 어플리케이션이 처리해야 할 데이터가 아니라, 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지를 알려주는 정보이다. Annotation 은 다음 세 가지 용도로 사용된다고 한다.

 

 

  • 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공한다
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공한다
  • 실행 시 특정 기능을 실행하도록 정보를 제공한다

 

 

Annotation 은 element 를 멤버로 가질 수 있다. 각 element 는 타입과 이름으로 구성되며, 디폴트 값을 가질 수 있다. element 의 타입으로는 primitive 타입들과, String, 열거 타입, Class 타입, 그리고 이들의 배열 타입을 사용할 수 있다고 한다. element 의 이름 뒤에는 메서드를 작성하는 것처럼 () 를 붙여야 한다. 아래와 같이 작성해볼 수 있는 것이다.

 

 

 

package thisisjava;

public @interface AnnotationTest {
    String elementTest1();
    int elementTest2() default 3;
}

 

 

 

위와 같이 정의한 어노테이션을 코드에 적용할 때는 아래와 같이 작성한다.

 

 

 

@AnnotationTest(elementTest1 = "Value", elementTest2 = 5)
// or
@AnnotationTest(elementTest1 = "Value")

 

 

 

elementTest1 은 디폴트 값이 없기 때문에 값을 필수로 넣어주어야 하고, elementName2 는 디폴트 값이 있기 때문에 생략이 가능한 것이다. 또, Annotation 은 기본 element 인 value 를 가질 수 있다.

 

 

 

public @interface AnnotationTest {
    String value();
    int elementTest2() default 3;
}

 

 

 

Value element 를 가진 Annotation 을 코드에 적용할 때는 아래와 같이 값만 작성해도 된다.

 

 

 

@AnnotationTest("Value")

 

 

 

Annotation 을 적용할 수 있는 대상은 java.lang.annotation.ElementType 열거 상수로 다음과 같이 정의되어 있다고 한다.

 

 

 

ElementType 열거 상수 적용 대상
TYPE 클래스, 인터페이스, 열거 타입
ANNOTATION_TYPE 어노테이션
FIELD 필드
CONSTRUCTOR 생성자
METHOD 메서드
LOCAL_VARIABLE 로컬 변수
PACKAGE 패키지

 

 

 

Annotation 이 적용될 대상을 지정할 때는 @Target 어노테이션을 사용한다. @Target 의 기본 엘리먼트인 value 는 ElementType 배열을 값으로 갖는다. 어노테이션이 적용될 대상을 복수 개로 지정할 수 있는 것이다. 아래와 같이 적용해볼 수 있다.

 

 

 

package thisisjava;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationTest {
    String value();
    int elementTest2() default 3;
}

 

 

 

위와 같이 작성해주면 클래스, 필드, 메서드에 어노테이션을 적용할 수 있게 되는 것이다. 

 

 

또, 어노테이션 정의 시 한 가지 더 추가해야할 내용으로 사용 용도에 따라 해당 어노테이션을 어느 범위까지 유지할 것인지를 지정해야 한다. 소스상에만 유지할 것인지, 컴파일된 클래스까지 유지할 것인지, 아니면 런타임 시에도 유지할 것인지를 지정해야 한다. 어노테이션 유지 정책은 java.lang.annotation.RetentionPolicy 열거 상수로 다음과 같이 정의되어 있다.

 

 

 

RetentionPolicy 열거 상수 Description
SOURCE 소스상에서만 어노테이션 정보를 유지한다. 소스 코드를 분석할 때만 의미가 있으며, 바이트 코드 파일에는 정보가 남지 않는다.
CLASS 바이트 코드 파일까지 어노테이션 정보를 유지한다. 리플렉션을 이용해서 어노테이션 정보를 얻을 수는 없다.
RUNTIME 바이트 코드 파일까지 어노테이션 정보를 유지하면서, 리플렉션을 이용해서 런타임 시에도 어노테이션 정보를 얻을 수 있다.

 

 

 

위 설명에서 Reflection (리플렉션) 이란 런타임 시에 클래스의 메타 정보를 얻는 기능을 뜻한다. 클래스가 가지고 있는 필드가 무엇인지, 어떤 생성자를 갖고 있는지, 어떤 메서드를 갖고 있는지, 적용된 어노테이션이 무엇인지 알아내는 것이 Reflection 이라고 한다. 이 유지 정책을 적용할 때는 @Retention 을 사용한다. @Retention 의 기본 element value 는 RetentionPolicy 타입이므로 위 상수 중 하나를 지정하면 된다.

 

 

 

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
    String value();
    int elementTest2() default 3;
}

 

 

 

그 뒤로는 런타임 시에 어노테이션이 적용되었는지 확인하고, element 값을 이용해서 특정 작업을 수행하는 방법에 대해 알아본다. Annotation 자체는 아무런 동작을 가지지 않는 표식일 뿐이지만, Reflection 을 이용해서 annotation 의 적용 여부와 element 의 값을 읽고 적절히 처리할 수 있다. 클래스에 적용된 어노테이션 정보를 얻는 것은 java.lang.Class 를 이용하면 되지만, 필드, 생성자, 메서드에 적용된 어노테이션 정보를 얻으려면 Class 이 아래 메서드를 통해 java.lang.reflect 패키지의 Field, Constructor, Method 타입의 배열을 얻어야 한다.

 

 

 

Returns Method Description
Field[] getFields() 필드 정보를 Field 배열로 리턴
Constructor[] getConstructors() 생성자 정보를 Constructor 배열로 리턴
Method[] getDeclaredMethods()  메서드 정보를 Method 배열로 리턴

 

 

 

그런 다음 Class, Field, Constructor, Method 가 가지고 있는 아래 메서드를 호출해서 적용된 어노테이션 정보를 얻을 수 있다.

 

 

 

Returns Description
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
지정한 어노테이션이 적용되었는지에 대한 여부. Class 에서 호출했을 때 상위 클래스에 적용된 경우에도 true 를 리턴한다.
Annotation getAnnotation(Class<T> annotationClass)
지정한 어노테이션이 적용되어 있으면 어노테이션을 리턴하고 그렇지 않다면 null 을 리턴한다. Class 에서 호출했을 때 상위 클래스에 적용된 경우에도 어노테이션을 리턴한다.
Annotation[] getAnnotations()
적용된 모든 어노테이션을 리턴한다. Class 에서 호출했을 때 상위 클래스에 적용된 어노테이션도 모두 포함한다. 적용된 어노테이션이 없을 경우 길이가 0 인 배열을 리턴한다.
Annotation[] getDeclaredAnnotations()
직접 적용된 어노테이션을 리턴한다. Class 에서 호출했을 때 상위 클래스에 적용된 어노테이션은 포함되지 않는다.

 

 

 

위 메서드들을 이용하여 간단한 어노테이션을 아래와 같이 생성해본다. 아래는 각 메서드의 실행 내용을 구분선으로 분리해서 콘솔에 출력하도록 하는 PrintAnnotation 이다.

 

 

 

package thisisjava;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
    String value() default "=";
    int number() default 15;
}

 

 

 

그 다음 위 어노테이션을 적용할 메서드들을 담은 클래스를 하나 작성한다.

 

 

 

package thisisjava;

public class AnnotationGo {
    @PrintAnnotation
    public void method1() {
        System.out.println("TEST1");
    }
    
    @PrintAnnotation("*")
    public void method2() {
        System.out.println("TEST2");
    }
    
    @PrintAnnotation(value="#", number = 20)
    public void method3() {
        System.out.println("TEST3");
    }
}

 

 

 

아래 코드는 Reflection 을 이용해서 AnnotationGo 클래스에 적용된 어노테이션 정보를 읽고 엘리먼트 값에 따라 출력할 문자와 출력 횟수를 콘솔에 출력한 후, 해당 메서드를 호출한다. method.invoke(new AnnotatinoGo()) 는 AnnotationGo 객체를 생성하고, 생성된 객체의 메서드를 호출하는 코드이다.

 

 

 

public class Main {
    public static void main(String[] args) {
        Method[] declaredMethods = AnnotationGo.class.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(PrintAnnotation.class)){
                PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);
                System.out.println("["+method.getName()+"]");
                StringBuffer temp = new StringBuffer();
                for (int i = 0; i < printAnnotation.number(); i++) {
                    temp.append(printAnnotation.value());
                }
                System.out.println(temp);

                try {
                    method.invoke(new AnnotationGo());
                } catch (Exception e) {
                    System.out.println();
                }
            }
        }
    }
}

 

 

result

 

 

 

 

 

 

Reference:

이것이 자바다 (신용권의 Java 프로그래밍 정복)

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

HttpServletRequest, HttpServletResponse  (0) 2023.04.30
Java (try-with-resources)  (0) 2023.04.29
Java (Primitive Type, Reference Type ~)  (0) 2023.04.25
Java Gson  (0) 2023.04.21
Java Enum  (0) 2023.04.19