공유된 기술노트 · 자바 · 1. 기초
자바 어노테이션: 설정을 코드 옆에 붙이는 방법
1. 어노테이션이란 무엇인가
"Annotation"이라는 단어의 사전적 의미는 '주석', '메모', '표시'입니다. 그렇다면 우리가 평소 코딩할 때 사용하는 // 또는 /* */ 주석과 같은 것일까요?
결론부터 말하면, 아닙니다.
- 일반 주석 (
//): 사람이 코드를 읽을 때 이해를 돕기 위해 작성합니다. 컴파일러는 이 주석을 무시하고, 컴파일 후 생성되는.class파일에는 주석 내용이 포함되지 않습니다. 즉, 실행 시점에 사라집니다. - 어노테이션 (
@): 사람이 읽기도 하지만, 다른 프로그램(소프트웨어)이 실행 시점에 읽기 위해 남겨둔 메타데이터(Metadata)입니다.
"코드를 읽는 프로그램"이 누구인가?
- 자바 컴파일러:
@Override같은 어노테이션을 보고 "이 메서드가 부모 클래스의 메서드를 오버라이드하는 것이 맞는지"를 검증합니다. - 프레임워크 (Spring, JPA 등):
@Autowired같은 어노테이션을 보고 "이 필드에 어떤 객체를 주입해야 하는지"를 판단합니다. - 우리가 만들 SQL 생성기: 필드에 붙인
@Column어노테이션을 보고 "이 필드는 데이터베이스의 어떤 컬럼에 매핑되는지"를 판단합니다.
즉, 어노테이션은 사람을 위한 메모가 아니라, 소프트웨어를 위한 명령어 또는 정보입니다.
2. 어노테이션을 사용하는 용도는?
이전 편에서는 db-config.properties라는 외부 파일을 만들어 테이블명과 컬럼명을 설정했습니다.
tableName=t_user
columnName.username=user_name이 방식은 좋지만, 코드를 보는 사람이 "이 필드가 실제로 어떤 컬럼으로 매핑되는지"를 알기 위해 코드와 파일을 번갈아 봐야 하는 번거로움이 있습니다.
어노테이션은 이 문제를 해결합니다.
- 외부 파일 방식: 설정이 코드와 분리되어 있어, 코드를 읽는 순간 매핑 관계를 알 수 없습니다.
- 어노테이션 방식: 설정(매핑 정보)을 코드 바로 옆에 붙여 놓습니다.
@Column("user_name")
private String username;이렇게 하면 username 필드를 보는 순간, 이것이 데이터베이스의 user_name 컬럼에 해당한다는 것을 한눈에 알 수 있습니다. "왜 파일 대신 코드 옆에 붙이는 게 좋은가?"에 대한 답은 바로 가독성과 결합도입니다.
3. 어노테이션의 특징
- 독자적인 주체: 어노테이션을 읽는 주체는 소프트웨어(번역기, 우리가 만드는 도구, 자바 VM, 프레임워크, 컴파일러)입니다.
- 정의가 필수: 사람이 임의로
@MyCustomNote라고 붙여 놓는 주석과 달리, 정의되지 않은 어노테이션을 사용하면 컴파일 에러가 발생합니다.- 예:
@Column을 사용하려면,Column이라는 어노테이션을 먼저 정의해 두어야 합니다.
- 예:
- 구조화된 데이터: 단순한 텍스트가 아니라, 값(인자)을 가질 수 있는 구조화된 데이터입니다.
4. 어노테이션은 누가 정의하는가?
어노테이션은 코드에 읽는 도구나 환경을 만드는 곳에서 정의를 합니다. 우리가 설정을 갖는 도구를 만든다면 그 설정을 위한 어노테이션은 우리가 정의합니다.
자바 실행환경이 정의한 어노테이션 (Built-in):
@Override: 메서드가 오버라이드되었음을 명시.@Deprecated: 더 이상 사용되지 않는 코드임을 경고.@SuppressWarnings: 컴파일러의 경고를 무시.- 이들은 우리가 정의할 필요 없이 바로 사용할 수 있습니다.
우리가 직접 정의하는 것 (Custom):
- 이번 편에서 다룰 내용입니다.
- "누가 정의하느냐"에 따라 "누가 읽느냐"가 정해집니다. 우리가
@Column을 정의하면, 우리가 만든SqlGenerator프로그램이 그 값을 읽어옵니다.
5. 어노테이션을 정의하기 위한 도구는?
자바에서 어노테이션을 정의하는 문법은 @interface입니다.
public @interface Column {
String value();
}왜 interface라고 부르는가?
- 어노테이션은 내부적으로 인터페이스를 기반으로 동작합니다.
@interface뒤에 오는 이름(Column)이 인터페이스의 이름이 됩니다.String value();는 이 어노테이션이 가질 수 있는 속성(멤버)을 정의합니다.value는 특별한 이름으로, 사용 시@Column("user_name")처럼 괄호 안에 값을 넣으면 이value에 저장됩니다.- 만약
name이라고 정의했다면@Column(name="user_name")처럼 사용해야 합니다.
6. 이전 예제에서 다루었던 외부 설정을 어노테이션으로 바꾸면?
이제 User 클래스와 SqlGenerator를 수정하여, properties 파일 대신 어노테이션을 사용하도록 변경해 보겠습니다.
1. Column 어노테이션 정의
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) // 필드에만 사용 가능
@Retention(RetentionPolicy.RUNTIME) // 실행 시점까지 유지 (리플렉션으로 읽기 위해)
public @interface Column {
String value();
}@Target(ElementType.FIELD): 이 어노테이션은 클래스나 메서드가 아닌, 필드에만 붙일 수 있음을 명시.@Retention(RetentionPolicy.RUNTIME): 컴파일 후에도 클래스 파일에 정보를 남기고, JVM 실행 시 리플렉션으로 읽을 수 있도록 설정. (이것이 없으면 실행 시점에 어노테이션 정보가 사라져 읽을 수 없음)
2. User 클래스 수정
properties 파일의 columnName.username=user_name 설정을 필드에 직접 붙입니다.
public class User {
private int id;
@Column("user_name") // username 필드는 user_name 컬럼에 매핑
private String username;
@Column("user_email") // email 필드는 user_email 컬럼에 매핑
private String email;
// getter/setter 생략
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getId() { return id; }
public void setId(int id) { this.id = id; }
}3. AnnotationSqlGenerator 생성
이제 리플렉션으로 필드를 읽을 때, 필드에 @Column 어노테이션이 있는지 확인하고, 있으면 그 값을 컬럼명으로 사용하도록 코드를 작성합니다.
import java.lang.reflect.Field;
public class AnnotationSqlGenerator {
public String generateInsertSql(Object obj) throws Exception {
String className = obj.getClass().getSimpleName();
// 테이블명은 여전히 클래스 이름 소문자로 가정 (또는 별도 어노테이션으로 확장 가능)
String tableName = className.toLowerCase();
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(tableName).append(" (");
Field[] fields = obj.getClass().getDeclaredFields();
boolean first = true;
for (Field field : fields) {
if (java.lang.reflect.Modifier.isStatic(field.getModifiers()))
continue;
// 1. 필드에 @Column 어노테이션이 있는지 확인
Column columnAnnotation = field.getAnnotation(Column.class);
String columnName;
if (columnAnnotation != null) {
// 2. 어노테이션이 있으면, 그 value를 컬럼명으로 사용
columnName = columnAnnotation.value();
} else {
// 3. 어노테이션이 없으면, 필드 이름 그대로 사용
columnName = field.getName();
}
if (!first) {
sql.append(", ");
}
sql.append(columnName);
first = false;
}
sql.append(") VALUES (");
// VALUES 부분의 ? 개수는 필드 개수와 동일
for (int i = 0; i < fields.length; i++) {
if (i > 0)
sql.append(", ");
sql.append("?");
}
sql.append(")");
return sql.toString();
}
}4. 실행 결과 비교
App.java (실행용 main 함수)
public class App {
public static void main(String[] args) {
// / 1. User 객체 생성 및 값 설정
User user = new User();
user.setId(1);
user.setUsername("john_doe");
user.setEmail("john@example.com");
// 2. SQL 생성기 초기화
AnnotationSqlGenerator generator = new AnnotationSqlGenerator();
// 3. INSERT SQL 생성 및 출력
try {
String sql = generator.generateInsertSql(user);
System.out.println("생성된 SQL:");
System.out.println(sql);
} catch (Exception e) {
e.printStackTrace();
}
}
}Case 1: properties 파일 기반 (이전 편)
db-config.properties에columnName.username=user_name- 결과:
INSERT INTO user (id, user_name, user_email) VALUES (?, ?, ?) - 단점: 설정 파일이 없으면 기본 필드명(
username)이 사용됨
Case 2: 어노테이션 기반 (이번 편)
User클래스에@Column("user_name")- 결과:
INSERT INTO user (id, user_name, user_email) VALUES (?, ?, ?) - 장점: 설정 파일 없이도, 코드를 컴파일하는 순간 매핑 정보가 고정됨
7. 어노테이션은 어디에 사용하는가 / 외부 파일보다 어떤 장점이 있는가?
장점
- 가독성 향상: 필드 바로 옆에 매핑 정보가 있으므로, 개발자가 코드를 읽는 순간 데이터베이스 스키마와의 관계를 파악할 수 있습니다.
- 파일 관리의 부재:
db-config.properties파일을 별도로 생성하고, 경로(src/main/resources/...)를 설정하고, 배포 시 파일을 함께 올리는 번거로움이 없습니다. - IDE 지원: IntelliJ IDEA나 Eclipse 같은 IDE는 어노테이션을 인식하여, 잘못된 값이나 누락된 어노테이션을 컴파일 전에 경고해 줄 수 있습니다.
한계 (주의점)
- 변경의 어려움: properties 파일은 배포 후에도 운영자가 값을 수정하여 재시작만 하면 됩니다. 하지만 어노테이션은 코드의 일부이므로, 값을 변경하려면 소스를 수정하고 다시 컴파일, 배포해야 합니다.
- 환경별 설정 분리: 개발 환경(DB1)과 운영 환경(DB2)의 컬럼명이 다를 경우, 어노테이션 하나로 두 환경을 모두 대응하기 어렵습니다. (이 경우
@Column과 properties를 함께 사용하거나, 프로파일 기반 설정을 사용해야 합니다.)
요약: 어노테이션은 "코드의 구조와 관계"를 코드 안에 명시적으로 표현할 때 강력합니다. 반면, "환경에 따라 자주 바뀌는 값"은 여전히 외부 설정 파일이 더 적합할 수 있습니다. 두 방식은 서로 배타적이지 않고, 목적에 따라 섞어서 사용할 수 있습니다.
이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.