자바 공부 기록 2회독(8) - 2024.1.25

동준·2024년 1월 25일
0

개인공부(자바)

목록 보기
10/16

7. java.base 모듈

라이브러리랑 모듈 파트에서 잠깐 들여봤던 자바 설치에서 따라오는 < 17 >이란 명칭의 기본 라이브러리는 앞으로 자바로 프로젝트를 할 때마다 자주 들여볼 라이브러리이자 기본 모듈이다.

오늘은 그 기본 모듈에 대해 중요한 부분(특히, Java 17 맟 그 언저리 버전(?)을 기점으로 추가된 기능들)들에 대해 숙지해야지.

여담으로, java.base는 모든 모듈이 의존하는 모듈이다.
심지어 requires 문법 적용이 없어도 사용할 수 있다.

1) 최상위 Object 클래스

자바의 프로젝트 내에서 존재하는 모든 클래스들의 조상님이 되는 Object 클래스가 존재한다. 심지어 System 클래스, String 클래스 등, 가볍게 작성하던 객체 참조 타입들 역시 Object 클래스가 조상님이다.

자바 프로젝트 내의 모든 클래스는 Object 클래스를 상속받는다.

(1) Object 클래스의 메소드

Object 클래스 내의 메소드는 3가지가 있다

  • eqauls(Object obj) : 객체의 동일 여부 참, 거짓 판단
  • hashCode() : 객체를 식별하는 해시코드 정수 반환
  • toString() : 객체를 표현하는 문자열(클래스명@16진수해시코드) 반환

이 메소드들은 Object 클래스를 상속받는 모든 클래스의 특성상, 어떤 클래스에서든지 재정의가 가능하다.

public class ObjectExample {
    private String id;
    private int number;
    private String info;

    public ObjectExample(String id, int number, String info) {
        this.id = id;
        this.number = number;
        this.info = info;
    }

    @Override
    // Object 클래스의 메소드 equals(Object object) 오버라이딩
    public boolean equals(Object object) {
        if(object instanceof ObjectExample objectExample) {
            if (id.equals(objectExample.id)) {
                return true;
            }
        }

        return false;
    }

    @Override
    // Object 클래스의 메소드 hashCode() 오버라이딩
    public int hashCode() {
        return number + id.hashCode();
    }

    @Override
    // Object 클래스의 메소드 toString() 오버라이딩
    public String toString() {
        return id + " : " + info;
    }
}

(2) 레코드 선언

자바 프로젝트에서의 설계 패턴과 관련해서 많이 나오는 키워드 중에 DTO라는 것이 있다. DTO는 Data Transfer Object의 약자로, 데이터 전송 목적으로 만들어진 객체를 의미한다.

DTO들은 보통 데이터 전송이라는 일관화된 목적으로 작성되기 때문에, 내부 은닉화된 필드들이 달라도 생성자와 메소드(주로 getter)들은 전부 동일하게 작성되기 마련이라서 반복적인 코드의 작성이 이뤄지게 되는데, 이를 간단하게 작성할 수 있는 기능이 Java 14에서 도입된 레코드다.

예를 들어서, 레코드를 작성하지 않고 DTO를 작성하면 이러하다.

public class Person {
	private final String name;
    private final int age;
    private final String id;
    
    public Person(String name, int age, String id) {
    	this.name = name;
        this.age = age;
        this.id = id;
    }
    
    public String name() {
    	return this.name;
    }
    
    public int age() {
    	return this.age;
    }
    
    public String id() {
    	return this.id;
    }
    
    @Override
    public int hashCode() { }
    
    @Override
    public boolean equals(Object obj) { }
    
    @Override
    public String toString() { }
}

이걸 레코드를 활용해서 작성하면 이러하다.

public record RecordExample(String name, int age, String id) {
}

끝, 어때요 참 쉽죠?
이렇게 작성해도 똑같이 new 연산자를 통한 객체 생성, 조회 전부 다 가능하다.

public class MainExample {
    public static void main(String[] args) {
        // 생성자
        RecordExample record = new RecordExample("winter", 30, "snow");
        RecordExample anotherRecord =
                new RecordExample("summer", 30, "ocean");

        // getter
        System.out.println(record.name());
        System.out.println(record.age());
        System.out.println(record.id());

        // 오버라이딩 메소드
        System.out.println(record.equals(anotherRecord));
        System.out.println(record.hashCode());
        System.out.println(record.toString()); 
        // 사실 이미 println 메소드 내에서 toString() 메소드를 사용 후 리턴
    }
}

(3) lombok 라이브러리

롬복은 코드 자동 생성 라이브러리로, 레코드처럼 주로 DTO를 작성할 때 많이 쓰인다.

근데 문제가 있다.

내가 설치해서 적용을 열심히 해봤는데... 잘 안된다... ㅠ
특히, 생성자 쪽이 문제다.아니 왜 안 되냐고요

최신 버전을 설치해도 안 되고, 플러그인으로 세팅을 시도해도 안 되고 있다.
아마 빌드 관련한 내용이 겹치는 것 같은데.... 음....
롬복을 쓸 일이 많을 것 같으니(아님 걍 레코드 써...?) 추후에 빌드 관련해서 공부하고 다시 도전하기로...

2) 기본적인 클래스

사실 기본적인 클래스 내용과 메소드들을 전부 외우는 것은 물리적으로 불가능하기 떄문에 이 부분을 세심하게 공부하는 것보다는 계속 프로젝트 연습을 하면서 손에 익히는 것이 더 중요하다고 생각했다.

물론 한 번 작성하고 버릴 문서가 전혀 아니므로, 필요하거나 중요한 내용이 있으면 업데이트 할 예정

3) 리플렉션

자바 클래스 중에 Class 객체가 존재한다.
이것의 역할이 무엇인고 하니, 클래스와 인터페이스가 보유한 패키지 정보, 타입 정보, 멤버(생성자, 필드, 메소드) 정보 등등(전부 묶어서 메타 정보라고 한다)을 관리한다.

이런 메타 정보를 프로그램에서 읽고 수정하는 행위를 리플렉션이라고 한다. 리플렉션을 수행하기 위해서는 우선 해당 클래스를 Class 객체 참조변수에 담아야 된다. 예를 들어서

package section_12_base_module.ex11;

import java.util.Objects;

public class Car {
    private String carName;
    private int price;
    private String driver;
    private int velocity;

    // 필드를 초기화하는 생성자
    public Car(String carName, int price, String driver, int velocity) {
        this.carName = carName;
        this.price = price;
        this.driver = driver;
        this.velocity = velocity;
    }

    // 매개변수 없는 생성자
    public Car() {
        // 기본적으로 아무 동작도 하지 않음
    }

    // Getter 메소드들
    public String getCarName() {
        return carName;
    }

    public int getPrice() {
        return price;
    }

    public String getDriver() {
        return driver;
    }

    public int getVelocity() {
        return velocity;
    }

    // Setter 메소드들
    public void setCarName(String carName) {
        this.carName = carName;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public void setVelocity(int velocity) {
        this.velocity = velocity;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return price == car.price && velocity == car.velocity && Objects.equals(carName, car.carName) && Objects.equals(driver, car.driver);
    }

    @Override
    public int hashCode() {
        return Objects.hash(carName, price, driver, velocity);
    }

    @Override
    public String toString() {
        return "Car{" +
                "carName='" + carName + '\'' +
                ", price=" + price +
                ", driver='" + driver + '\'' +
                ", velocity=" + velocity +
                '}';
    }
}

아씨 롬복쓰면 노가다 불필요한데
임의로 작성한 Car 클래스가 있다. 이것에 대한 메타 정보들을 리플렉션하려면

Class classInfo = Car.class;

이렇게 Class 객체 참조 변수에 담는다.

(1) 패키지 정보 읽기

해당 클래스가 위치하는 패키지 정보를 얻기 위해서는 관련 메소드를 활용한다.
콘솔 출력을 위해서 System 클래스의 println() 메소드를 활용했다.

System.out.println("패키지 : " + classInfo.getPackageName());
System.out.println("클래스 간단 이름 : " + classInfo.getSimpleName());
System.out.println("클래스 전체 이름 : " + classInfo.getName());

(2) 멤버 정보 읽기

생성자 정보 읽기

생성자의 정보를 읽으려면 Constructor[] getDeclaredConstructors() 메소드를 활용한다.

Constructor[] constructors = classInfo.getDeclaredConstructors();
// constructors 배열 안에 Car 클래스의 생성자들이 담기게 된다.
// 하나하나 읽으려면 반복문을 쓰던가... 뭐 기타 등등...

필드 정보 읽기

필드의 정보를 읽으려면 Field[] getDeclaredFields() 메소드를 활용한다.

Field[] fields = classInfo.getDeclaredFields();
// fields 배열 안에 Car 클래스의 필드들이 담기게 된다.
// 출력하면 타입까지 출력된다.
// 하나하나 읽으려면 반복문을 쓰던가... 뭐 기타 등등...

메소드 정보 읽기

메소드의 정보를 읽으려면 Method[] getDeclaredMethods() 메소드를 활용한다.

Method[] methods = classInfo.getDeclaredMethods();
// methods 배열 안에 Car 클래스의 메소드들이 담기게 된다.
// 매개변수가 존재하면 매개변수(타입을 포함해서)도 같이 출력된다.
// 하나하나 읽으려면 반복문을 쓰던가... 뭐 기타 등등...

(3) 리소스 정보 읽기

클래스 파일을 기준으로 다른 리소스 파일(이미지, XML, Property 파일)의 정보를 상대 경로의 형식으로 얻을 수 있다. 사용 메소드는 아래와 같다.

URL getResource(String name)
// 리소스 파일의 URL 리턴

InputStream getResourceAsStream(String name)
// 리소스 파일의 InputStream 리턴

4) 어노테이션

어노테이션은 간단하게 말해서 설정 정보다.
설정 정보라고 함은, 클래스나 인터페이스를 컴파일 혹은 실행할 때 어떻게 처리할 것인지에 대한 정보라고 할 수 있다.

보통은 다음 3가지의 용도로 쓰인다

  • 컴파일 시 사용하는 정보 전달
  • 빌드 툴이 코드를 자동을 생성할 때 사용하는 정보 전달
  • 실행 시 특정 기능을 처리할 때 사용하는 정보 전달

상속과 인터페이스에서 메소드를 오버라이딩할 때 쓰던 기호인 @Override 역시 대표적인 어노테이션이며, 모든 어노테이션은 앞에 @를 붙여준다.

(1) 어노테이션 타입

어노테이션 역시 하나의 타입이다. 그렇기 때문에 직접 어노테이션을 정의해서 설정 정보를 담을 수 있다. 정의 방법은 인터페이스 정의 방법과 유사하다.

public @interface AnnotationName {
}

@AnnotationName
// 위처럼 정의한 어노테이션은 코드에서 이렇게 사용한다

(2) 어노테이션의 속성

사실 @만 하나 붙인다고 해서 모든 설정 정보를 담을 수 있는 건 아니라고 생각했는데, 공부하는 교재에서는 어노테이션 속성 내용이 꽤 빈약해서 좀 더 구글링하면서 내용을 찾아봤다.그리고 뭔 소린지 모르겠다

내가 이해하기로는 인터페이스의 추상 메소드를 정의하되, 기본 속성값을 추가적으로 부여할 수 있는 기능으로 받아들였다. 간단한 예시를 들어보면...

public @interface AnnotationName {
	String prop1();
    int prop2() default 1;
}

어디서 많이 봤다 싶더라니... 인터페이스의 추상 메소드와 똑같다.
이렇게 정의한 어노테이션은 코드에서 다음과 같이 사용할 수 있다. prop1은 기본값이 없기 때문에 반드시 값을 기술해야 하고, prop2는 기본값이 있으므로 생략이 가능하며, 기본값 이외의 값을 부여할 수도 있다.

@AnnotationName(prop1="값");

@AnnotationName(prop2="값", prop2=3);
// prop2에 새로운 값 기술

또한, 어노테이션에는 기본 속성인 value를 가지게 된다.

public @interface AnnotationName {
	String value();
    int prop2() default 1;
}

value 속성을 가진 어노테이션은 값만 기술한다. 하지만, value 속성과 다른 속성의 값을 준다면 value 속성 이름을 반드시 언급한다.

@AnnotationName(value="값", prop2=3);

(3) 어노테이션 적용과 유지

어노테이션은 설정 정보이며, 이 설정 정보는 어떤 대상에게 적용이 되어야 의미가 있다. 그 어떤 대상이라 함은, 클래스가 될 수도 있고, 메소드가 될 수도 있고, 필드가 될 수도 있고... 다양하며, 각각에게 명시를 해줘야 한다.

적용 대상의 종류는 ElemetnType 열거 상수로 정의된다.

또한, 어노테이션이 적용됐으면, 이 어노테이션을 언제까지 유지할 것인지에 대해서도 지정이 필요하다. 통상 적용 시점과 제거 시점으로 구별하며, 그 기준은 컴파일이나 로딩, 프로그램 실행이 된다.

유지 정책의 종류는 RetentionPolicy 열거 상수로 정의된다.

밑에는 ElementType 열거 상수와 RetentionPolicy 열거 상수로 둬서 임의의 어노테이션을 만드는 예시다.

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; // 출력 횟수
}

이제 이 어노테이션을 실제로 적용을 해본다. 위의 경우는 메소드에 적용하며, 유지는 런타임(실행) 후, 계속 유지된다. 속성으로 value 속성이 있기 때문에 새로운 값이 아닌 이상 명칭을 특별히 기술하지는 않고, number 속성은 기본값이 15로 설정됐다.

public class Service {
    @PrintAnnotation
    public void method1() {
        System.out.println("run method_1");
    }

    @PrintAnnotation("*")
    public void method2() {
        System.out.println("run method_2");
    }

    @PrintAnnotation(value = "#", number = 20) // value에 다른 속성의 값을 동시에 주고 싶으면 value 기술
    public void method3() {
        System.out.println("run method_3");
    }
}

어노테이션을 적용한 메소드를 보유한 클래스를 리플렉션해서 어노테이션의 설정 정보를 얻어내고, 구분선을 출력해본다.

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PrintAnnotationExample {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
        Method[] declaredMethods = Service.class.getDeclaredMethods();
        for (Method method: declaredMethods) {
            PrintAnnotation printAnnotation = method.getAnnotation(
                    PrintAnnotation.class
            );
            // getAnnotation(AnnotationName.class) : 지정한 어노테이션 적용되어있으면 어노테이션 리턴, 안 그러면 null 리턴

            printLine(printAnnotation);
            method.invoke(new Service()); // 메소드 호출
            printLine(printAnnotation);
        }
    }

    private static void printLine(PrintAnnotation printAnnotation) {
        if (printAnnotation != null) {
            int number = printAnnotation.number(); // number() 속성값 얻기
            for (int i=0; i<number; i++) {
                String value = printAnnotation.value();
                System.out.print(value);
            }
            System.out.println();
        }
    }
}

아마 프로젝트 개발할 때 어노테이션을 엄청 자주 쓰게 될 것 같은데, value 속성에 대한 이해가 아직 부족하다. 이 부분을 좀 더 추가로 공부해야 됨....

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글