공유된 기술노트 · 자바 · 1. 기초

newlec · 2026. 9. 23.

자바 어노테이션: 설정을 코드 옆에 붙이는 방법

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. 어노테이션의 특징

  1. 독자적인 주체: 어노테이션을 읽는 주체는 소프트웨어(번역기, 우리가 만드는 도구, 자바 VM, 프레임워크, 컴파일러)입니다.
  2. 정의가 필수: 사람이 임의로 @MyCustomNote라고 붙여 놓는 주석과 달리, 정의되지 않은 어노테이션을 사용하면 컴파일 에러가 발생합니다.
    • 예: @Column을 사용하려면, Column이라는 어노테이션을 먼저 정의해 두어야 합니다.
  3. 구조화된 데이터: 단순한 텍스트가 아니라, 값(인자)을 가질 수 있는 구조화된 데이터입니다.

4. 어노테이션은 누가 정의하는가?

어노테이션은 코드에 읽는 도구나 환경을 만드는 곳에서 정의를 합니다. 우리가 설정을 갖는 도구를 만든다면 그 설정을 위한 어노테이션은 우리가 정의합니다.

  1. 자바 실행환경이 정의한 어노테이션 (Built-in):

    • @Override: 메서드가 오버라이드되었음을 명시.
    • @Deprecated: 더 이상 사용되지 않는 코드임을 경고.
    • @SuppressWarnings: 컴파일러의 경고를 무시.
    • 이들은 우리가 정의할 필요 없이 바로 사용할 수 있습니다.
  2. 우리가 직접 정의하는 것 (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.propertiescolumnName.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. 어노테이션은 어디에 사용하는가 / 외부 파일보다 어떤 장점이 있는가?

장점

  1. 가독성 향상: 필드 바로 옆에 매핑 정보가 있으므로, 개발자가 코드를 읽는 순간 데이터베이스 스키마와의 관계를 파악할 수 있습니다.
  2. 파일 관리의 부재: db-config.properties 파일을 별도로 생성하고, 경로(src/main/resources/...)를 설정하고, 배포 시 파일을 함께 올리는 번거로움이 없습니다.
  3. IDE 지원: IntelliJ IDEA나 Eclipse 같은 IDE는 어노테이션을 인식하여, 잘못된 값이나 누락된 어노테이션을 컴파일 전에 경고해 줄 수 있습니다.

한계 (주의점)

  • 변경의 어려움: properties 파일은 배포 후에도 운영자가 값을 수정하여 재시작만 하면 됩니다. 하지만 어노테이션은 코드의 일부이므로, 값을 변경하려면 소스를 수정하고 다시 컴파일, 배포해야 합니다.
  • 환경별 설정 분리: 개발 환경(DB1)과 운영 환경(DB2)의 컬럼명이 다를 경우, 어노테이션 하나로 두 환경을 모두 대응하기 어렵습니다. (이 경우 @Column과 properties를 함께 사용하거나, 프로파일 기반 설정을 사용해야 합니다.)

요약: 어노테이션은 "코드의 구조와 관계"를 코드 안에 명시적으로 표현할 때 강력합니다. 반면, "환경에 따라 자주 바뀌는 값"은 여전히 외부 설정 파일이 더 적합할 수 있습니다. 두 방식은 서로 배타적이지 않고, 목적에 따라 섞어서 사용할 수 있습니다.

이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.