[JAVA] 이것이 자바다_8
Updated:
객체 지향 프로그래밍의 네번째 정리.
접근 제한자, Getter와 Setter 메소드, 어노테이션에 대해 알아보자.
접근 제한자
main() 메소드를 가지지 않는 대부분의 클래스는 외부 클래스에서 이용할 목적으로 설계된 라이브러리 클래스이다.
라이브러리 클래스를 설계할 때에는 외부 클래스에서 접근할 수 있는 멤버와 접근할 수 없는 멤버로 구분해서 필드, 생성자, 메소드를 설계하는 것이 바람직하다.
객체 생성을 막기 위해 생성자를 호출하지 못하게 하거나 객체의 특정 데이터를 보호하기 위해 해당 필드에 접근하지 못하도록 막아야 한다.
그리고 특정 메소드를 호출할 수 없도록 제한할 필요가 있다.
자바는 이러한 기능을 구현하기 위해 접근 제한자(Access Modifier) 를 제공하고 있다.
접근 제한자는 public, protected, default, private와 같이 네 가지 종류가 있다.
접근 제한 | 적용 대상 | 접근할 수 없는 클래스 |
---|---|---|
public | 클래스, 필드, 생성자, 메소드 | 없음 |
protected | 필드, 생성자, 메소드 | 자식 클래스가 아닌 다른 패키지에 소속된 클래스 |
default | 클래스, 필드, 생성자, 메소드 | 다른 패키지에 소속된 클래스 |
private | 필드, 생성자, 메소드 | 모든 외부 클래스 |
클래스의 접근 제한
클래스를 선언할 때 고려해야 할 사항은 같은 패키지 내에서만 사용할 것인지, 아니면 다른 패키지에서도 사용할 수 있도록 할 것인지를 결정해야 한다.
클래스에 적용할 수 있는 접근 제한은 public과 dafault 단 두 가지인데, 다음과 같은 형식으로 작성한다.
// default 접근 제한
class 클래스 { ... }
// public 접근 제한
public class 클래스 { ... }
default 접근 제한
클래스를 선언할 때 public을 생략했다면 클래스는 default 접근 제한을 가진다.
클래스가 default 접근 제한을 가지게 되면 같은 패키지에서는 아무런 제한 없이 사용할 수 있지만 다른 패키지에서는 사용할 수 없도록 제한된다.
public 접근 제한
클래스를 선언할 때 public 접근 제한자를 붙였다면 클래스는 public 접근 제한을 가진다.
클래스가 public 접근 제한을 가지게 되면 같은 패키지뿐만 아니라 다른 패키지에서도 아무런 제한 없이 사용할 수 있다.
생성자의 접근 제한
객체를 생성하기 위해서는 new 연산자로 생성자를 호출해야 한다.
하지만 생성자를 어디에서나 호출할 수 있는 것은 아니다.
생성자가 어떤 접근 제한을 갖느냐에 따라 호출 가능 여부가 결정된다.
접근 제한자 | 생성자 | 설명 |
---|---|---|
public | 클래스(…) | public 접근 제한은 모든 패키지에서 아무런 제한 없이 생성자를 호출할 수 있도록 한다. 클래스가 default 접근 제한을 가진다면 클래스 사용이 같은 패키지로 한정되므로, 비록 생성자가 public 접근 제한을 가지더라도 같은 패키지에서만 생성자를 호출할 수 있다. |
protected | 클래스(…) | protected 접근 제한은 default와 마찬가지로 같은 패키지에 속하는 클래스에서 생성자를 호출할 수 있도록 한다. 차이점은 다른 패키지에 속한 클래스가 해당 클래스의 자식 클래스라면 생성자를 호출할 수 있다. |
default | 클래스(…) | 생성자를 선언할 때 public 또는 private를 생략했다면 생성자는 default 접근 제한을 가진다. 같은 패키지에서는 아무런 제한 없이 생성자를 호출할 수 있으나, 다른 패키지에서는 생성자를 호출할 수 없도록 한다. |
private | 클래스(…) | 동일 패키지이건 다른 패키지이건 상관없이 생성자를 호출하지 못하도록 제한한다. 따라서 클래스 외부에서 new 연산자로 객체를 만들 수 없다. 오로지 클래스 내부에서만 생성자를 호출할 수 있고, 객체를 만들 수 있다. |
필드와 메소드의 접근 제한
필드와 메소드를 선언할 때 고려해야 할 사항은 클래스 내부에서만 사용할 것인지, 패키지 내에서만 사용할 것인지, 아니면 다른 패키지에서도 사용할 수 있도록 할 것인지를 결정해야 한다.
이것은 필드와 메소드가 어떤 접근 제한을 갖느냐에 따라 결정된다.
필드와 메소드는 다음과 같이 public, protected, default, private 접근 제한을 가질 수 있다.
접근 제한자 | 생성자 | 설명 |
---|---|---|
public | 필드 메소드(…) | public 접근 제한은 모든 패키지에서 아무런 제한 없이 필드와 메소드를 사용할 수 있도록 해준다. 필드와 메소드가 public 접근 제한을 가질 경우 클래스도 public 접근 제한을 가져야 한다. |
protected | 필드 메소드(…) | protected 접근 제한은 default와 마찬가지로 같은 패키지에 속하는 클래스에서 필드와 메소드를 사용할 수 있도록 한다. 차이점은 다른 패키지에 속한 클래스가 해당 클래스의 자식 클래스라면 필드와 메소드를 사용할 수 있다. |
default | 필드 메소드(…) | 필드와 메소드를 선언할 때 public 또는 private를 생략했다면 생성자는 default 접근 제한을 가진다. 같은 패키지에서는 아무런 제한 없이 필드와 메소드를 사용할 수 있으나, 다른 패키지에서는 필드와 메소드를 사용할 수 없도록 한다. |
private | 필드 메소드(…) | 동일 패키지이건 다른 패키지이건 상관없이 필드와 메소드를 사용하지 못하도록 제한한다. 오로지 클래스 내부에서만 사용할 수 있다. |
Getter와 Setter 메소드
일반적으로 객체 지향 프로그래밍에서 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 막는다.
그 이유는 객체의 데이터를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성(결점이 없는 성질)이 깨어질 수 있기 때문이다.
예를 들어 자동차의 속도는 음수가 될 수 없는데, 외부에서 음수로 변경하면 객체의 무결성이 깨진다.
이러한 문제를 해결하기 위해 객체 지향 프로그래밍에서는 메소드를 통해서 데이터를 변경하는 방법을 선호한다.
데이터는 외부에서 접근할 수 없도록 막고 메소드는 공개해서 외부에서 메소드를 통해 데이터에 접근하도록 유도한다.
그 이유는 메소드는 매개값을 검증해서 유효한 값만 데이터로 저장할 수 있기 때문이다.
이러한 역할을 하는 메소드가 Setter 이다.
예를 들어 자동차의 속도를 setSpeed() 메소드로 변경할 경우 다음과 같이 검증 코드를 작성할 수 있다.
void setSpeed(double spped){
if(speed < 0){
this.speed = 0;
return ;
} else{
this.speed = speed;
}
}
외부에서 객체의 데이터를 읽을 때도 메소드를 사용하는 것이 좋다.
객체 외부에서 객체의 필드값을 사용하기에 부적절한 경우도 있다.
이런 경우에는 메소드로 필드값을 가공한 후 외부로 전달하면 된다.
이런 메소드가 바로 Getter 이다.
예를 들어 자동차의 속도를 마일에서 KM 단위로 환산해서 외부로 리턴해주는 getSpeed() 메소드를 다음과 같이 작성할 수 있다.
double getSpeed(){
double km = speed * 1.6;
return km;
}
클래스를 선언할 때 가능하다면 필드를 private로 선언해서 외부로부터 보호하고, 필드에 대한 Setter와 Getter 메소드를 작성해서 필드값을 안전하게 변경/사용하는 것이 좋다.
다음은 Setter와 Getter 메소드를 선언하고 사용하는 예시이다.
// Car.java
public class Car{
// 필드
private int speed;
private boolean stop;
// 생성자
// 메소드
public int getSpeed(){
return speed;
}
public void setSpeed(int speed){
if(speed < 0){
this.speed = 0;
return;
} else{
this.speed = speed;
}
}
public boolean isStop(){
return stop;
}
public void setStop(boolean stop){
this.stop = stop;
this.speed = 0;
}
}
// CarExample.java
public class CarExample(){
public static void main(String[] args){
Car myCar = new Car();
// 잘못된 속도 변경
myCar.setSpeed(-50);
System.out.println("현재 속도: " + myCar.getSpeed());
// 올바른 속도 변경
myCar.setSpeed(60);
// 멈춤
if(!myCar.isStop()){
myCar.setStop(true);
}
System.out.println("현재 속도: " + myCar.getSpeed());
}
}
어노테이션
어노테이션(Annotaion)은 메타데이터(metadata)라고 볼 수 있다.
메타데이터란 애플리케이션이 처리해야 할 데이터가 아니라, 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지를 알려주는 정보이다.
어노테이션은 다음과 같은 형태로 작성된다.
@AnnotationName
어노테이션은 다음 세 가지 용도로 사용된다.
- 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공
- 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공
- 실행 시(런타임 시) 특정 기능을 실행하도록 정보를 제공
컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공하는 대표적인 예는 @Override 어노테이션이다.
@Override 는 메소드 선언 시 사용하는데, 메소드가 오버라이드(재정의)된 것임을 컴파일러에게 알려주어 컴파일러가 오버라이드 검사를 하도록 해준다.
정확히 오버라이드가 되지 않았다면 컴파일러는 에러를 발생시킨다.
어노테이션은 빌드 시 자동으로 XML 설정 파일을 생성하거나, 배포를 위해 JAR 압축 파일을 생성하는데에도 사용된다.
그리고 실행 시 클래스의 역할을 정의하기도 한다.
어노테이션 타입 정의와 적용
어노테이션 타입을 정의하는 방법은 인터페이스를 정의하는 것과 유사하다.
다음과 같이 @interface를 사용해서 어노테이션을 정의하며, 그 뒤에 사용할 어노테이션 이름이 온다.
public @interface AnnotationName{
}
이렇게 정의한 어노테이션은 코드에서 다음과 같이 사용한다.
@AnnotationName
어노테이션은 엘리먼트(element)를 멤버로 가질 수 있다.
각 엘리먼트는 타입과 이름으로 구성되며, 디폴트 값을 가질 수 있다.
public @interface AnnotationName{
타입 elementName() [default 값];
...
}
엘리먼트의 타입으로는 int나 double과 같은 기본 데이터 타입이나 String, 열거 타입, Class 타입, 그리고 이들의 배열 타입을 사용할 수 있다.
엘리먼트의 이름 뒤에는 메소드를 작성하는 것처럼 ()를 붙여야 한다.
예를 들어 String 타입의 엘리먼트와 int 타입의 엘리먼트를 다음과 같이 선언한다.
public @interface AnnotationName{
String elementName1();
int elementName2() default 5;
}
이렇게 정의한 어노테이션을 코드에서 적용할 때에는 다음과 같이 기술한다.
@AnnotationName(elementName1="값", elementName2=3);
또는
@AnnotationName(elementName1="값");
어노테이션 적용 대상
어노테이션을 적용할 수 있는 대상은 java.lang.annotation.ElementType 열거 상수로 다음과 같이 정의되어 있다. |ElementType 열거 상수|적용 대상| |:—-|:—-| |TYPE|클래스, 인터페이스, 열거 타입| |ANNOTATION_TYPE|어노테이션| |FIELD|필드| |CONSTRUCTOR|생성자| |METHOD|메소드| |LOCAL_VARIABLE|로컬 변수| |PACKAGE|패키지|
어노테이션이 적용될 대상을 지정할 때에는 @Target 어노테이션을 사용한다.
@Target의 기본 엘리먼트인 value는 ElementType 배열을 값으로 가진다.
이것은 어노테이션이 적용될 대상을 복수개로 지정하기 위해서이다.
예를 들어 다음과 같이 어노테이션을 적용할 경우,
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName{
}
다음과 같이 클래스, 필드, 메소드만 어노테이션을 적용할 수 있고 생성자는 적용할 수 없다.
@AnnotationName
public class ClassName{
@AnnotationName
private String fieldName;
// @AnnotationName --- @Target에 CONSTRUCT가 없어 생성자는 적용 못함
public ClassName() { }
@AnnotationName
public void methodName() { }
}
어노테이션 유지 정책
어노테이션 정의 시 한 가지 더 추가해야 할 내용은 사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정해야 한다.
소스상에만 유지할 건지, 컴파일된 클래스까지 유지할 건지, 런타임 시에도 유지할 건지를 지정해야 한다.
어노테이션 유지 정책은 java.lang.annotation.RetentionPolicy 열거 상수로 다음과 같이 정의되어 있다.
RetentionPolicy 열거 상수 | 설명 |
---|---|
SOURCE | 소스상에서만 어노테이션 정보를 유지한다. 소스 코드를 분석할 때만 의미가 있으며, 바이트 코드 파일에는 정보가 남지 않는다. |
CLASS | 바이트 코드 파일까지 어노테이션 정보를 유지한다. 하지만 리플렉션을 이용해서 어노테이션 정보를 얻을 수는 없다. |
RUNTIME | 바이트 코드 파일까지 어노테이션 정보를 유지하면서 리플렉션을 이용해서 런타임 시에 어노테이션 정보를 얻을 수 있다. |
리플렉션(Reflection) 이란 런타임 시에 클래스의 메타 정보를 얻는 기능이다.
예를 들어 클래스가 가지고 있는 필드가 무엇인지, 어떤 생성자를 갖고 있는지, 어떤 메소드를 가지고 잇는지, 적용된 어노테이션이 무엇인지 알아내는 것이 리플렉션이다.
리플렉션을 이용해서 런타임 시에 어노테이션을 얻으려면 어노테이션 유지 정책을 RUNTIME으로 설정해야 한다.
어노테이션 유지 정책을 지정할 때에는 @Retention 어노테이션을 사용한다.
우리가 작성하는 어노테이션은 대부분 런타임 시점에 사용하기 위한 용도로 만들어진다.
다음은 런타임 유지 정책을 적용한 어노테이션이다.
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName{
}
런타임 시 어노테이션 정보 사용하기
런타임 시에 어노테이션이 적용되었는지 확인하고 엘리먼트 값을 이용해서 특정 작업을 수행하는 방법에 대해 알아보자.
어노테이션 자체는 아무런 동작을 가지지 않는 단지 표식일 뿐이지만, 리플렉션을 이용해서 어노테이션의 적용 여부와 엘리먼트 값을 읽고 적절히 처리할 수 있다.
클래스 에 적용된 어노테이션 정보를 얻으려면 java.lang.Class 를 이용하면 되지만,
필드, 생성자, 메소드 에 적용된 어노테이션 정보를 얻으려면 java.lang.reflect 패키지의 Field, Constructor, Method 타입의 배열을 얻어야 한다.
리턴 타입 | 메소드명(매개 변수) | 설명 |
---|---|---|
Field[] | getFields() | 필드 정보를 Field 배열로 리턴 |
Constructor[] | getConstructors() | 생성자 정보를 Constructor 배열로 리턴 |
Method[] | getDeclaredMethods | 메소드 정보를 Method 배열로 리턴 |
다음은 어노테이션과 리플렉션을 이용해서 간단한 예제를 만들어 보자.
다음은 각 메소드의 실행 내용을 구분선으로 분리해서 콘솔에 출력하도록 하는 PrintAnnotation이다.
// PrintAnnotation.java (어노테이션 정의)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation{
String value() default "-";
int number() default 15;
}
// Service.java (어노테이션 적용)
public class Service{
@PrintAnnotation
public void method1(){
System.out.println("실행 내용1");
}
@PrintAnnotation("*")
public void method2(){
System.out.println("실행 내용2");
}
@PrintAnnotation(value="#", number=20)
public void method3(){
System.out.println("실행 내용3");
}
}
public class PrintAnnotationExample{
public static void main(String[] args){
// Service 클래스로부터 메소드 정보를 얻음
Method[] declaredMethods = Service.class.getDeclaredMethods();
// Service 클래스에 선언된 메소드 얻기(리플렉션)
// Method 객체를 하나씩 처리
for(Method method : declaredMethods){
// PrintAnnotation이 적용되었는지 확인
if(method.isAnnotationPresent(PrintAnnotation.class)){
// PrintAnnotation 객체 얻기
PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);
// 메소드 이름 출력
System.out.println("[" + method.getName() + "] ");
// 구분선 출력
for(int i=0; i<printAnnotation.number(); i++){
System.out.println(printAnnotation.value());
}
System.out.println();
try{
// 메소드 호출
method.invoke(new Service());
} catch (Exception e) { }
System.out.println();
}
}
}
}
객체 지향 프로그래밍의 접근 제한자, Getter, Setter, 어노테이션을 정리해보았다.
어노테이션을 정의할 수도 있다는 걸 알게 되었다.
잘 쓰지는 않을 것 같은데 한 번은 쓰지 않을까..
꼼꼼히 공부하니 힘들다..
Leave a comment