현실 세계에서 어떤 제품을 만들 때, 부품을 만들고 이를 조립해서 완성된 제품을 만들 듯이, SW를 개발할때도 부품에 해당하는 객체
를 만들고, 이것들을 하나씩 조립해서 완성된 프로그램을 만드는 기법을 객체 지향 프로그래밍(OOP)라고 한다.
(추가할 내용이 많겠지만, 일단 그런 걸로 🙄)
객체(Object)란 물리적으로 존재하거나, 추상적으로 생각할 수 있는 것 중에 속성
을 가지고 식별 가능한 것
을 말한다.
객체는 속성
과 동작
으로 구성되어 있다.
자바에서는 이를 필드
와 메소드(method)
라고 부른다.
- 객체들은 각각
독립적
으로 존재하고, 다른 객체와 서로 상호작용 하면서 동작한다. - 객체들 사이의 상호작용 수단은
메소드
이다. - 메소드 호출은 다음과 같은 형태를 가진다.
리턴값 = 객체.메소드(매개값1, 매개값2 ...)
- 매개값은 메소드를 실행하기 위해 필요한 데이터이다.
- 리턴값은 메소드가 실행하고 나서 호출한 곳으로 돌려주는 값이다.
- 객체는 개별적으로 사용 될 수 있지만, 대부분 다른 객체와
관계
를 맺는다. 집합
,사용
,상속
관계가 있다.- 집합: 하나는 부품이고 하나는 완성품 (ex 자동차 - 핸들, 엑셀 ...)
- 사용: 객체는 다른 객체의 메소드를 호출하여 원하는 결과를 얻어낸다 (ex 사람 - 자동차)
- 상속: 부모 객체를 기반으로 하위 객체를 생성하는 관계, 일반적으로 부모는 종류를, 자식 객체는 구체적 사물에 해당한다 (ex 자동차 - 기계)
객체의 필드, 메소드를 하나로 묶고, 실제 구현 내용을 감추는 것
외부 객체는 내부의 구조를 알지 못하며, 제공하는 필드와 메소드만 이용할 수 있다.
외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 한다.
이를 위해 접근 제한자
를 사용한다.
부모가 가지고 있는 재산을 자식에게 물려주는 것 부모 객체는 자신의 필드와 메소드를 하위 객체에게 물려주어 사용할 수 있도록 한다. 부모 객체 재사용으로 하위 객체 빠르고 쉽게 설계한다. 즉, 반복된 코드를 줄여준다
다형성은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 하나의 타입에 여러 객체를 대입함으로써 다양한 기능을 이용하게 한다. 자바는 다형성을 위해 부모 클래스, 인터페이스의 타입 변환을 허욯나다. 부모 타입에는 모든 자식객체가 대입 될 수 있고. 모든 인터페이스 타입의 구현객체가 대입 될 수 있다. (LSP)
ex) (인터페이스) 자동차의 바퀴 - (구현체) 경주용, 일반용
사용하고 싶은 객체가 있다면 설계도(Java의 Class)가 필요하다. 클래스(설계도)에는 객체를 생성하기 위한 필드/메소드가 정의되어 있다. 클래스로부터 만들어진 객체를 해당 클래스의 인스턴스(instance)라고 한다. (ex 자동차 객체 -> 자동차 클래스의 인스턴스) 클래스로부터 ㅇ객체를 만드는 과정을 인스턴스화라고 한다. 객체 지향 프로그래밍 개발은 세가지 단계가 있다.
- 클래스 설계
- 클래스를 가지고 사용할 객체를 생성
- 객체 이용
자바의 식별자 작성 규칙에 따라 만들어야 한다.
클래스 이름이 단일 단어라면 첫 자를 대문자로, 나머지를 소문자로 작성한다.
만약 서로 다른 단어가 혼합된 이름을 사용한다면, 각 단어의 첫 머리 글자를 대문자로 작성하는 것이 관례다.
주의할 점은 파일 이름과 동일한 이름의 클래스 선언에만 public
접근 제한자를 붙일 수 있다.
가급적이면 소스 파일 하나당 동일한 이름의 클래스 하나를 선언하는 것이 좋다.
자세한 사항은 Oracle 자바 네이밍 컨벤션을 참고하자
클래스로부터 객체를 생성하는 방법은 다음과 같이 new
연산자를 사용하면 된다.
클래스 변수명 = new 클래스();
new
는 클래스로부터 객체를 생성시키는 연산자이다.new
연산자 뒤에는 생성자가 오는데, 생성자는 클래스() 형태를 가지고 있다.new
연산자로 생성된 객체는 메모리 힙 영역에 생성된다. (객체 주소를 리턴한다.)
클래스는 두가지 용도가 있다. **라이브러리(API)**와 실행용으로 나뉜다. 프로그램 전체에서 사용되는 클래스가 100개라면 99개는 라이브러리이고 나머지는 실행 클래스이다. 분리 안할 수 있지만, 분리해야 한다.
라이브러리는 다른 클래스에서 이용할 목적으로 설계된다. 실행 클래스는 프로그램 실행 진입점인 main() 메소드를 제공한다.
클래스의 구성멤버에는 필드
, 생성자
, 메소드
가 있다.
public class ClassName{
//필드
int fieldName;
//생성자
ClassName();
//메소드
void methodName();
}
객체의 데이터가 저장되는 곳
고유 데이터, 부품 객체, 상태 정보를 저장하는 곳이다.
선언 형태는 변수와 비슷하지만, 필드
를 변수라고 부르지 않는다.
필드와 변수의 차이 변수는 생성자와 메소드 내에서만 사용되고, 생성자와 멤소드가 실행 종료되면 자동 소멸된다. 하지만
필드
는 생성자, 메소드 전체에 사용되며 객체 소멸 전까지 존재한다.
객체 생성 시 초기화 역할 담당한다. 필드를 초기화 하거나, 메소드를 호출해서 객체를 사용할 준비를한다. 생성자는 메소드와 비슷하게 생겼지만 클래스 이름으로 되어있고, 리턴값이 없다.
메소드는 객체의 동작에 해당되는 블록이다.
메소드를 호출하게 되면 중괄호 블록에 있는 모든 코드들이 일괄적으로 실행된다.
메소드는 필드
를 읽고 수정하는 역할도 하지만, 다른 객체를 생성해 다양한 기능을 수행 하기도 한다.
메소드는 객체 간의 데이터 전달의 수단으로 사용된다.
외부로부터 매개값을 받거나, 값을 리턴할 수도 있다.3
다시 한번, 필드와 변수는 다르다. 또, 클래스 멤버 변수라는 표현도 있지만, 필드로 사용하자.
- 초기값이 지정되지 않은 필드들은 객체 생성시 자동으로 기본 초기값으로 설정된다.
- 기본 타입(정수, 실수, 논리)는
0, OL, 0.0, \u0000(빈 공백) 등
으로 초기화 된다. - 참조 타입은
null
로 초기화된다. - 클래스 내부에서는 필드명으로 접근할 수 있다.
- 그러나 필드를 클래스 외부에서 사용하려면, 객체를 생선한 뒤 필드를 사용해야 한다.
- 필드는
객체
에 소속된 데이터이므로객체
가 존재하지 않으면 필드도 존재하지 않는다.
생성자는 new
연산자와 같이 사용되어 클래스로부터 객체를 생성할 때 호출되어 객체의 초기화를 담당한다.
생성자를 실행시키지 않고는 클래스로부터 객체를 만들 수 없다.
new
연산자에 의해 생성자가 실행되면 힙 영역에 객체가 생성되고 그 주소가 리턴된다.
만약 에러가 발생하면 객체는 생성되지 않는다.
모든 클래스는 생성자가 반드시 존재하며, 하나 이상을 가질 수 있다.
클래스가 public
클래스로 선언되면 기본 생성자에도 public
이 붙지만, 클래스가 public
생성자가 없이 클래스로만 선언되면 기본 생성자에도 public
이 붙지 않는다.
그러나 클래스에 명시적으로 선언한 생성자가 한 개라도 있으면, 컴파일러는 기본 생성자를 추가하지 안늖다.
매개 변수 선언은 생략할 수도 있고, 여러개를 선언해도 좋다.
public class Car{
// codes..
// 생성자
Car(String model, String color, int maxSpeed);
}
Car myCar = new Car("그랜저", "검정", 300);
클래스에 생성자가 명시적으로 선언되어 있을 경우에는 반드시 선언된 생성자를 호출해서 객체를 생성해야만 한다.
클래스로부터 객체가 생성될 때 필드는 기본 초기값으로 자동 설정된다. 만약 다른 값으로 초기화하고 싶다면 두가지가 있다.
//1. 필드 선언할 때 초기화
pulbic class Korean{
String nation = "대한민국";
String name;
String ssn;
}
Korean k1 = new Korean();
soutv(k1.nation); //대한민국
//2. 생성 시점 초기화
public class Korean{
//fields..
//constructor
public Korean(String n, String s){
name = n;
ssn = s;
}
}
첫번째 예시의 경우 객체를 생성하면 nation 필드에 "대한민국"이 저장되어 있다. 그러나 객체 생성 시점에 다양한 값을 가져야 한다면, 두번째 처럼 생성자를 만들어 주어야 한다.
매개변수 이름이 짧으면 가독성이 좋지 않기에, 초기화시킬 필드 이름과 비슷하거나 동일한 이름으로 사용할 것을 권한다. 다만, 동일한 이름으로 매개변수를 지정하면 해당 필드에 접근할 수 없다. 매개 변수 우선순위가 높기 때문이다.
이때, this
를 사용하여 접근할 수 있다.
public class Korean{
//fields..
//constructor
public Korean(String name, String ssn){
this.name = name;
this.ssn = ssn;
//필드 매개변수
}
}
객체의 필드는 하나가 아니라 여러 개가 있고, 이 필드들을 모두 생성자에서 초기화한다면, 생성자엔 필드만큼의 매개변수가 요구된다. 그러나 실제로는 중요한 몇 필드만 매개변수를 통해 초기화 되고, 나머지 필드들은 필드 선언시 초기화되거나 생성자 내부에서 임의의 값 또는 계산된 값으로 초기화 된다. 아니면 객체 생성 후에 필드값을 별도로 저장하기도 한다.
생성자 오버로딩이란 매개 변수를 달리하는 생정자를 여러 개 선언하는 것을 말한다.
public class Car{
Car(){...}
Car(String model){...}
Car(String model, String color){...}
Car(String model, String color, int maxSpeed){...}
}
생성자 오버로딩이 많아질 경우 생성자 간 중복된 코드가 발생할 수 있다. 매개변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이러한 현상을 많이 볼 수 있다.
this
는 자신의 다른 생성자를 호출하는 코드로 반드시 생성자의 첫줄에서만 허용된다.
this()
다음에는 추가적인 실행문들이 올 수 있다.
public class Car{
Car(){}
Car(String model){
this(model, "은색", 250);
}
Car(String model, String color){
this(model, color, 250);
}
Car(String model, String color, int maxSpeed){
this.model = model;
this.color = color;
this.maxSpeed = maxSpeed;
}
}
객체에 동작에 해당하는 블록이다. 메소드를 호출하면 중괄호 블록에 있는 모든 코드들이 일괄적으로 실행된다.
메소드 선언은 선언부(리턴타입, 메소드이름, 매개변수선언)와 실행 블록으로 구성된다. (시그니처 라고도 한다)
-
리턴 타입 리턴 타입은 메소드가 실행 후 리턴하는 값의 타입을 말한다. 리턴값이 있을수도, 없을 수도 있다. 만약 리턴 값이 있다면, 리턴 받는 변수의 타입에 맞게(혹은 반대로) 반환해야 한다. 그러나 리턴 값이 있다고 꼭 저장할 필요는 없다.
-
메소드 이름 숫자로 시작하면 안되고, $와 _를 제외한 특수문자를 사용하면 안된다. 관례적으로 메소드 명은 소문자로 작성한다. 서로 다른 단어가 혼합된 이름이라면 뒤이어 오는 단어의 첫머리 글자는 대문자로 작성한다. (메소드 네이밍의 중요성은 다른 포스트에서 참고해야 한다.)
-
매개변수 선언
double divide(int x, int y){...}
이렇게 선언된 divide() 메소드를 호출할 떄에는 반드시 두 개의 int 값을 주어야 한다. (매개값은 반드시 매개 변수 타입에 부합되는 값이어야 한다.)
매개 변수의 수를 모를 경우 (ex. N개의 수를 합하는 메소드) 이럴땐, 매개 변수를 배열 타입으로 선언하면 된다. 그러나, 매개 변수를 배열 타입으로 선언하면, 메소드 호출 전에 배열을 생성해야 하는 불편한 점이 있다. "$\cdots$"를 사용하여 해결 할 수 있다.
int sum1(int[] values){...} int values1 = {1,2,3}; sum1(values1); sum1(new int[]{1,2,3});int sum2(int ⋯ values){...} int values2 = {1,2,3}; sum2(1,2,3); sum2(1,2,3,4,5); sum2(values2); sum2(new int[]{1,2,3});
메소드 선언에 리턴 타입이 있는 메소드는 반드시 리턴(return)문을 사용해서 리턴값을 지정해야 한다. 만약 return문이 없다면 컴파일 오류가 발생한다.
return문의 리턴 값은 리턴 타입이거나, 리턴 타입으로 변환 될 수 있어야 한다.
ex) 리턴 타입
int
returns byte, short, int => (O, 자동 형변환)
return문이 실행되면 메소드는 즉시 종료된다. 이후에 실행문이 오면 "Unreachable code"라는 컴파일 오류가 발생한다. (조건 분기시 제외)
리턴 값이 없는 메소드(void) void로 선언된 리턴값이 없는 메소드에서도 return 문을 사용할 수 있다. 다음과 같이 return문을 사용하면 메소드 실행을 강제 종료시킨다.
return;
메소드는 클래스 내-외부의 호출에 의해 실행된다. 클래스 내부에선 메소드 이름을 그대로 사용한다. 클래스 외부에선 객체를 생성한 뒤, 참조 변수를 이용해서 메소드를 호출해야 한다. (객체가 존재해야 메소드가 존재한다).
클래스 내에 같은 이름의 메소드를 여러개 선언하는 것을 오버로딩이라고 한다. 리턴 타입이 달라진다고, 매개변수 이름만 바뀐다고 오버로딩이 아니다. 생성자 오버로딩과 같아서 넘어간다.
인스턴스 멤버란 객체(인스턴스)를 생성한 후 사용할 수 있는 필드와 메소드를 말한다.
각 인스턴스 필드, 인스턴스 메소드라고 부른다.
인스턴스 필드는 객체마다 따로 스택에 존재한다.
인스턴스 메소드는 객체마다 존재하지 않고 메소드 영역에 저장되고 공유된다.
객체 내부에서도 인스턴스 멤버에 접근하기 위해 this
를 사용할 수 있다.
정적 멤버는 클래스에 고정된 멤버로서 객체를 생성하지 하지 않고 사용할 수 있는 필드와 메소드를 말한다. 정적 멤버는 객체(인스턴스)에 소속된 멤버가 아니라 클래스에 소속된 멤버이기에 클래스 멤버라고도 한다.
public class 클래스{
//정적 필드
static type field [= 초기값];
//정적 메소드
static 리턴타입 메소드( 매개변수 ...){...};
}
정적 필드, 메소드는 클래스에 고정된 멤버이므로 클래스 로더가 클래스(바이트 코드)를 로딩해서 메소드 메모리 영역에 적재할 때 클래스별로 관리된다. 따라서 클래스의 로딩이 끝나면 바로 사용할 수 있다.
인스턴스 필드: 객체마다 가지고 있어야 할 데이터 정적 필드: 공용적인 데이터 ex) 원주율(
$\pi$ )
인스턴스 메소드: 인스턴스 필드를 필요로 하는 메소드 정적 메소드: 인스턴스 필드를 필요로 하지 않는 메소드 ex 계산기의 연산은 정적 메소드, 계산기의 색깔 변경은 인스턴스 메소드
클래스 이름과 함께 도트 연산자(.)로 접근한다. 원칙적으로는 클래스 이름으로 접근해야 하지만, 객체 참조 변수로도 접근이 가능하다. (IDE에서 경고를 뿜는다.)
클래스.필드;
클래스.메소드(매개값 ...);
인스턴스.(정적)필드;
인스턴스.(정적)메소드;
정적 필드는 필드 선언과 동시에 초기값을 주는 것이 보통이다.
static double pi = 3.14159;
그러나, 계산이 필요한 초기화 작업이 있을 수 있다. 정적 필드는 객체 생성 없이 사용 가능해야 하므로 생성자에서 초기화 작업이 불가능하다. 자바는 정적 필드의 복잡한 초기화 작업을 위하여 정적 블록을 제공한다.
static {
//your code
}
정적 블록도 클래스가 메모리로 로딩될 때 자동으로 실행된다. 클래스 내부에 여러개 선언되어도 상관 없다. (선언된 순서로 실행된다.)
public classs TV{
static String compnay = "Samsung";
static String model = "LCD";
static String info;
static {
info = compnay + "-" + model;
}
}
객체가 없어도 실행된다는 특징 때문에, 내부에 인스턴스 필드/메소드 사용이 불가능하다. 또한 this 키워드도 불가능하다.
main() 메소드도 정적 메소드여서 인스턴스 필드와 인스턴스 메소드를 main() 메소드에서 바로 사용할 수 없다. ex)
public class Car{ int speed; void run(){...} public static void main(String[] args){ speed = 60; //컴파일 에러 run(); //컴파일 에러 } }
전체 프로그램에서 단 하나의 객체만 만들도록 보장해야 하는 경우가 있다.
이 객체를 싱글톤
이라고 한다.
싱글톤을 만드려면 클래스 외부에서 new
생성자로 생성자를 호출할 수 없도록 막아야 한다. 생성자를 호출한 만큼 객체가 생성되기 때문이다.
생성자를 외부에서 호출할 수 없도록 private
접근 제한자를 붙여주면 된다.
그리고 자신의 타입인 정적 필드
를 하나 선언하고 자신의 객체를 생성해 초기화 한다.
정적 필드
도 private
접근 제한자를 붙여 외부에서 필드값을 변경하지 못하도록 막는다.
대신 외부에서 호출할 수 있는 정적 메소드
인 getInstance()를 선언하고 정적 필드에서 참조하는 자신의 객체를 리턴해준다.
//정적 필드, 싱글톤 객체 생성
private static 클래스 singleton = new 클래스();
//생성자
private 클래스(){}
//정적 메소드
static 클래스 getInstance(){
return singleton;
}
// 두 변수는 같은 객체를 참조한다.
클래스 variable1 = 클래스.getInstance();
클래스 variable2 = 클래스.getInstance();
final
의 의미는 최종적이란 뜻을 가지고 있다.
final
필드는 초기값이 저장되면 이것이 최종적인 값이 되어서 프로그램 실행 도중에 수정할 수 없다.
final 타입 필드 [= 초기값];
final
필드의 초기값을 줄 수 있는 방법은 딱 두가지 밖에 없다.
- 필드 선언시 주는 방법
- 생성자에서 주는 방법
단순 값이라면 필드 선언시에 주는 것이 제일 간단하지만,
- 복잡한 초기화 코드가 필요하거나
- 외부 데이터로 최가화 해야한다면 생성자에서 초기값을 지정해줘야 한다.
생성자는 final 필드의 최종 초기화를 마쳐야한다.
만약 초기화되지 않은 final
필드를 그대로 남겨두면 컴파일 에러가 발생한다.
일반적으로 불변의 값을 상수라 부른다. 그렇다면 final 필드가 상수일까? 아니다. 불변의 값은 객체마다 저장할 필요가 없는 공용성을 띠기 때문이다. 그렇다면 static 필드가 상수일까? 아니다. static 값은 변경이 가능하기 때문이다. 그러므로 static final이 되어야한다.
static final 타입 상수 [= 초기값];
초기값이 단순 값이라면 선언시에 주는 것이 일반적이지만, 복잡한 초기화의 경우 정적 블록에서도 할 수 있다.
상수 이름은 모두 대문자로 작성하는 것이 관례다. 만약 서로 다른 단어가 혼합된 이름이라면 언더바(_)로이어준다. ex)
static final PI = 3.14159; static final EARTH_RADIUS = 6400; static final EARTH_SURFACE_AREA; static { EARTH_SURFACE_AREA = 4*PI*EARTH_RADIUS*EARTH_RADIUS; }
프로젝트 개발을 하다보면 많은 클래스를 작성하게 된다.
클래스를 체계적으로 관리하지 않으면 클래스간 관계
가 뒤엉켜서 복잡하고 난해해진다.
자바에서는 클래스를 체계적으로 관리하기 위해 패키지를 사용한다.
- 패키지는 단순히 파일 시스템의 폴더 기능만 하는 것이 아니라 클래스의 일부분이다.
- 패키지는 클래스를 유일하게 만들어주는 식별자 역할을 한다 (클래스의 전체 이름은 **"패키지명+클래스명"**이다.
패키지는 클래스를 컴파일하는 과정에서 자동으로 생성된다. 패키지 이름에도 컨벤션이 존재한다.
- 숫자로 시작해선 안되고 _(언더바), $를 제외한 특수문자 사용 금지
- java로 시작하는 패키지는 자바 표준 API에서 사용하므로 금지
- 모두 소문자로 작성
여러 회사가 프로젝트 개발에 참여하는 경우, 패키지 이름 중복 방지를 위해 흔히 회사의 도메인 이름으로 패키지를 만든다. 마지막엔 프로젝트 이름을 붙여주는게 관례이다.
패키지 선언이 포함된 클래스를 컴파일 할 경우, 단순히 javac ClassName.class
명령어로 컴파일 해서는 패키지가 폴더가 생성되지 않는다.
javac -d . ClassName.java <- 현재 폴더내에 생성
javac -d ..\bin ClassName.java <- 현재 폴더와 같은 위치의 bin에 생성
javac -d C:\Temp\bin ClassName.java <= C:\Temp\bin 폴더에 생성
같은 패키지라면 아무 조건 없이 다른 클래스를 사용할 수 있다. 다른 패키지의 클래스 접근은 두가지 중 하나를 선택해야 한다.
- 패키지와 클래스를 모두 기술하는 것
- import 사용
//모두 기술
public class Car{
com.hankook.Tire tire = new com.hankook.Tire();
}
//import
import com.hankook.Tire;
import com.mycompany.*;
public class Car{
Tire tire = new Tire();
}
단 *를 사용 했을때, 하위 패키지까지 불러오진 않는다. 패키지 이름 전체를 기술하는 첫 번째 방법이 꼭 필요한 경우는 서로 다른 패키지에 동일한 클래스 이름이 존재하는 경우이다. (assertThat)
접근 제한 | 적용 대상 | 접근할 수 없는 클래스 |
---|---|---|
public | 클래스, 필드, 생성자, 메소드 | 없음 |
protected | 필드, 생성자, 메소드 | 자식 클래스가 아닌 다른 패키지에 소속된 클래스 |
default | 클래스, 필드, 생성자, 메소드 | 다른 패키지에 소속된 클래스 |
private | 필드, 생성자, 메소드 | 모든 외부 클래스 |
클래스를 선언할때에 '같은 패키지에서만 사용할 것인지', '다른 패키지에서도 사용할수 있게할 것인지'를 결정해야 한다.
즉, 인터넷에 배포되는 라이브러리 클래스들도 모두 public 접근 제한을 갖는다.
접근 제한자 | 설명 |
---|---|
public | public 접근 제한은 모든 패키지에서 아무런 제한 없이 생성자를 호출 할 수 있도록 한다. 클래스가 default 접근 제한을 가진다면, 클래스 사용이 같은 패키지로 한정되므로, 비록 생성자가 public 접근 제한을 갖더라도 같은 패키지에서만 생성자를 호출할 수 있다. |
protected | default와 마찬가지로 같은 패키지에 속하는 클래스에서 생성자를 호출할 수 있도록 한다. 차이점은 다른 패키지의 클래스가 해당 클래스의 자식 클래스라면 생성자를 호출할 수 있다. |
default | 같은 패키지내에서는 아무런 제한없이 생성자를 호출할 수 있으나, 다른 패키지에서는 생성자를 호출할 수 없도록 한다. |
private | 동일 패키지, 다른 패키지이건 생성자를 호출하지 못하도록 제한한다. 클래스 외부에서 new 연산자로 객체를 만들 수 없다. 오로지 클래스 내부에서만 생성자를 호출할 수 있고, 객체를 만들 수 있다. |
가끔 전체 프로글매에서 단 하나의 객체만 만들도록 보장해야 하는 경우가 있다. 싱글톤 객체이다. 싱글톤 패턴은 생성자를 private 접근제한으로 선언하고, 자신의 유일한 객체를 리턴하는 getInstance() 정적 메소드를 선언하는것을 말한다.
필드와 메소드를 클래스 내부에서만 사용할지, 패키지내에서만 사용할 것인지, 패키지 외부에서도 사용할 것인지를 결정해야 한다.
//필드 선언
[ public | protected | private ] [static] 타입 필드;
//메소드 선언
[ public | protected | private ] [static] 리턴타입 메소드(...){...};
접근 제한자 | 설명 |
---|---|
public | public 접근 제한은 모든 패키지에서 아무런 제한 없이 필드와 메소드를 호출 할 수 있도록 한다. 클래스가 default 접근 제한을 가진다면, 클래스 사용이 같은 패키지로 한정되므로, 필드를 사용할 수 없게 된다. |
protected | default와 마찬가지로 같은 패키지에 속하는 클래스에서 필드와 메소드를 사용할 수 있도록 한다. 차이점은 다른 패키지의 클래스가 해당 클래스의 자식 클래스라면 필드와 메소드를 사용할 수 있다. |
default | 같은 패키지내에서는 아무런 제한없이 필드와 메소드를를 사용할 수 있으나, 다른 패키지에서는 필드와 메소드를 사용할 수 없도록 한다. |
private | 동일 패키지, 다른 패키지이건 필드와 메소드를 호출하지 못하도록 제한한다. 오로지 클래스 내부에서만 필드와 메소드를 사용할 수 있다. |
객체지향 프로그래밍에서는 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 막는다. 객체의 무결성(결점이 없는 성질)을 깨기 때문이다. 예를 들어 자동차의 속도가 음수가 되는 경우가 있다. 이런 문제점을 해결 하기 위해 메소드를 통해서 데이터를 변경한다. 데이터는 외부에서 직접 접근을 막고, 매개값을 점검하여 유효한 값만 데이터로 저장할 수 있도록 한다.
어노테이션은 메타데이터라고 볼 수 있다. 메타데이터란 애플리케이션이 처리해야 할 데이터가 아니라, 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지를 알려주는 정보이다.
@AnnotationName
어노테이션은 다음 세가지 용도로 사용된다.
- 컴파일러에게 코드 문법 에러를 체크하도록 정보 제공
- 소프트웨어 개발 툴이 빌드/배치시 코드를 자동으로 생성할 수 있도록 정보 제공
- 실행 시(런타임) 특정 기능을 실행하도록 정보를 제공
@Override 어노테이션을 많이 사용했을 것이다. @Override는 메소드 선언시 사용하는데, 메소드가 오버라이드(재정의) 된 것임을 컴파일럭에게 알려주어 컴파일러가 오버라이드 검사를 하도록 해준다.
어노테이션 타입을 정의하는 방법은 인터페이스를 정의하는 것과 유사하다.
- @interface를 사용해서 어노테이션을 정의한다.
- 어노테이션은 엘리먼트(element)를 멤버로 가질 수 있다.
- 각 엘리먼트는 타입과 이름으로 구성되며, 디폴트 값을 가질 수 있다.
public @interface AnnotationName{
//타입 elementName() [default 값];
String elementName1();
int elementName2() default 5;
}
// 둘중 하나만 해도 된다.
// 단, elementName1의 경우 디폴트 값이 없기에 반드시 기술해야 한다.
@AnnotationName(elementName1 = "값", elementName2 = 3);
@AnnotationName(elementName1 = "값");
java.alng.annotation.ElementType 열거 상수로 다음과 같이 정의되어 있다.
어노테이션이 적용될 대상을 지정할 때에는 @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가 없어 생성자 적용 X
public ClassName() {}
pulbic void methodName(){}
사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정해야한다. 쉽게 설명하면 소스상에서만 유지할 건지, 컴파일된 클래스 까지 유지할 건지, 런타임시에도 유지할 건지를 지정해야한다. java.lang.annotation.RetentionPolicy 열거 상수로 다음과 같이 지정 되어 있다.
리플렉션(Reflection)이란 런타임시에 클래스의 메타 정보를 얻는 기능을 말한다.
ex) 클래스가 가지고 있는 필드, 어떤 생성자를 ㄱ자는지, 어떤 메소드를 갖는지, 어떤 어노테이션이 적용되었는지.
리플렉션을 이용하여 런타임 시에 어노테이션 정보를 얻으려면 어노테이션 유지 정책을 RUNTIME으로 설정해야 한다. 어노테이션 유지 정책을 지정할 떄는 @Retention 어노테이션을 사용한다. @Retention 기본 엘리먼트인 value는 RetentionpPolicy 타입이므로 아래중 하나를 고르면 된다.
- SOURCE, CLASS, RUNTIME
코드 자동 생성 툴을 개발하지 않는 이상 우리가 작성하는 어노테이션은 대부분 런타임 시점에 사용하기 위한 용도로 만들어진다.
런타임 시에 어노테이션이 적용되었는지 확인하고 엘리먼트 값을 이용해서 특정 작업을 수행하는 방법에 대해 알아보자.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation{
String value() default "-";
int number() default 15;
}
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();
for(Method method : declaredMethods){
//어노테이션 적용 확인
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++){
soutv(printAnnotation.value());
}
soutv();
try{
method.invoke(new Service());
} catch(Exception e){
soutv(e.getMessage));
}
}
}
}
}