@Override 하나 붙인다고 코드가 달라질 거라고 생각하셨나요? 저도 처음엔 그냥 IDE가 붙여주는 장식 정도로 여겼습니다. 그런데 직접 커스텀 어노테이션을 만들고 리플렉션으로 클래스 동작을 제어해보니, 어노테이션이 단순한 메모가 아니라 실행 흐름 자체를 바꾸는 장치라는 걸 몸으로 이해하게 됐습니다.

어노테이션, 그냥 주석이 아니었습니다
어노테이션(Annotation)이란 Java 소스코드에 메타데이터(Metadata)를 추가하는 기능입니다. 여기서 메타데이터란 코드 자체의 로직은 아니지만, 코드가 어떻게 처리돼야 하는지 부가 정보를 제공하는 데이터를 의미합니다. @Override, @Deprecated처럼 JDK에서 기본 제공하는 것부터, @Autowired나 @RequestMapping처럼 Spring 프레임워크가 만들어둔 것, 그리고 @Getter나 @Setter처럼 Lombok 라이브러리에서 제공하는 것까지 종류가 상당히 많습니다.
일반적으로 어노테이션은 "컴파일러에게 알려주는 힌트 정도"라고 알려져 있지만, 제 경험상 이건 좀 다릅니다. @Retention(RetentionPolicy.RUNTIME)이 붙은 어노테이션은 컴파일 이후에도 JVM 런타임까지 살아남아서 실제로 실행 흐름을 제어하는 데 쓰입니다. 여기서 @Retention이란 어노테이션의 생존 범위를 결정하는 설정으로, SOURCE(컴파일 시 소멸), CLASS(.class 파일에는 존재하지만 실행 시 소멸), RUNTIME(실행 중에도 유지) 세 단계로 나뉩니다.
커스텀 어노테이션을 만들 때는 @Target도 함께 설정해야 합니다. @Target이란 해당 어노테이션을 클래스, 메소드, 필드, 매개변수 중 어디에 붙일 수 있는지 사용 위치를 제한하는 속성입니다. 이 두 가지를 빠뜨리면 어노테이션을 만들더라도 런타임에서 읽어들이지 못해서 아무 효과가 없거나 예상치 못한 곳에 달리는 상황이 생깁니다. 제가 처음 커스텀 어노테이션을 만들 때 @Retention을 빠뜨렸다가 리플렉션 코드에서 어노테이션을 인식하지 못해 한참 헤맸던 기억이 있습니다.
핵심 설정 항목을 정리하면 다음과 같습니다.
- @Target: 어노테이션 적용 위치 제한 (TYPE, FIELD, METHOD, PARAMETER 등)
- @Retention: 어노테이션 유지 시점 설정 (SOURCE, CLASS, RUNTIME)
- @interface: 어노테이션 타입을 선언하는 키워드
JDBC 연결과 MVC패턴, 왜 쪼개야 하는가
Postman으로 회원가입 요청을 보내고 Servlet에서 JSON을 파싱해 DB에 INSERT하는 코드를 한 파일에 몰아넣으면 처음엔 편합니다. 저도 그렇게 시작했습니다. 그런데 로그인 기능을 추가하려는 순간 문제가 터집니다. DB 연결 코드가 doPost() 안에 두 번 등장하고, 비즈니스 로직과 SQL이 뒤엉켜 어디를 고쳐야 할지 파악이 안 됩니다.
JDBC(Java Database Connectivity)란 Java 애플리케이션이 MariaDB 같은 관계형 데이터베이스와 통신하기 위한 표준 API입니다. DriverManager.getConnection()으로 Connection 객체를 생성하고, Statement 또는 PreparedStatement로 SQL을 실행한 뒤, SELECT의 경우 ResultSet으로 결과를 순회하는 구조입니다. 이 흐름 자체는 단순하지만, Servlet 하나에 JDBC 코드, JSON 파싱, 비즈니스 규칙이 전부 들어오면 단일 책임 원칙(SRP, Single Responsibility Principle)을 심각하게 위반하게 됩니다. SRP란 하나의 클래스는 하나의 책임만 가져야 한다는 객체지향 설계 원칙으로, 변경 이유가 하나여야 유지보수가 쉬워진다는 뜻입니다.
이 문제를 해결하는 대표적인 구조가 MVC 패턴과 레이어드 아키텍처의 조합입니다. MVC 패턴은 화면(View), 요청 처리(Controller), 데이터 교환 객체(Model)를 분리하는 설계 방식인데, Controller가 가진 역할이 여전히 너무 많아지는 문제가 남습니다. 그래서 Controller의 책임을 다시 계층으로 쪼개는 레이어드 아키텍처를 함께 적용합니다. 실제로 Java 웹 애플리케이션에서 Spring Boot를 기준으로 한 레이어드 아키텍처 채택 비율은 국내 백엔드 프로젝트의 상당수를 차지할 만큼 사실상 표준으로 자리 잡고 있습니다(출처: Baeldung).
레이어드 아키텍처는 역할을 세 계층으로 나눕니다.
- 프레젠테이션 계층(Controller): 클라이언트 요청을 받아 파라미터를 추출하고 응답을 반환
- 비즈니스 계층(Service): 실제 처리 로직, 유효성 검증, 조합 로직 담당
- 퍼시스턴스 계층(Repository): DB 연결 및 CRUD 쿼리 실행
이렇게 나누면 SQL 변경이 생겼을 때 Repository만, 로직 변경이 생겼을 때 Service만 수정하면 됩니다. OCP(Open/Closed Principle), 즉 개방 폐쇄 원칙에서 말하는 "확장에는 열려 있고 수정에는 닫혀 있는" 구조가 자연스럽게 만들어집니다.
Postman으로 검증하고, 리플렉션으로 이해한 것
Postman은 API 테스트 도구로, 브라우저나 프론트엔드 없이 HTTP 요청을 직접 서버에 보낼 수 있습니다. 제가 직접 써봤는데, 백엔드 개발 중에 "이 Servlet이 실제로 JSON을 제대로 받는가"를 확인하는 가장 빠른 방법이었습니다. raw 탭에서 JSON 형식을 선택하고 email, password, nickname을 담아 POST 요청을 보내면, ObjectMapper가 req.getReader()에서 읽어들인 스트림을 Signup 객체로 역직렬화해줍니다. 여기서 역직렬화(Deserialization)란 네트워크로 전달된 JSON 문자열을 Java 객체로 변환하는 과정으로, jackson-databind 라이브러리가 이 역할을 담당합니다.
솔직히 이건 예상 밖이었습니다. ObjectMapper 한 줄이 필드 이름을 기준으로 JSON 키와 Java 멤버 변수를 자동 매핑해준다는 게 처음엔 마법처럼 느껴졌거든요. 내부적으로는 리플렉션(Reflection) 기술을 사용합니다. 리플렉션이란 런타임에 클래스 구조를 코드로 탐색하고 조작하는 기술로, private 접근 제한자가 걸린 필드에도 접근할 수 있어 프레임워크나 라이브러리 내부에서 광범위하게 활용됩니다. Java 공식 문서에서도 리플렉션 API는 프레임워크, 직렬화, 디버거 등에 활용되는 핵심 기능으로 명시되어 있습니다(출처: Oracle Java Documentation).
제 경험상 커스텀 어노테이션은 처음 만들 때보다 "왜 만들어야 하는지"를 이해한 이후에 훨씬 유용하게 쓸 수 있었습니다. 특정 클래스에 이름표를 붙이고 리플렉션으로 그 이름표를 읽어 동작을 분기하는 패턴은, Spring이 @Controller나 @Service를 읽어 빈을 등록하는 방식과 본질적으로 같습니다. 원리를 알고 나면 프레임워크의 동작이 더 이상 블랙박스처럼 느껴지지 않습니다.
어노테이션과 MVC패턴, 레이어드 아키텍처는 개념만 따로 외울 때보다 실제로 코드를 짜면서 "왜 이렇게 나눠야 하는지"를 느낄 때 비로소 제대로 자리를 잡는 것 같습니다. Postman으로 요청을 쏘고 콘솔에 찍힌 결과를 확인하는 과정이 지루해 보여도, 그 과정에서 HTTP 요청이 어떻게 객체로 변환되고 DB에 닿는지 흐름 전체가 눈에 들어오기 시작합니다. 일단 직접 손으로 만들어보는 것, 그게 가장 빠른 방법입니다.