Java 문법을 배울 때는 클래스를 만들고, 필드를 선언하고, getter, setter, toString() 같은 메서드를 직접 작성했다. 작은 예제에서는 직접 작성해도 큰 문제가 없었지만, 클래스 수가 늘어나면 같은 형태의 코드가 반복된다.
Spring Boot 프로젝트를 시작하면서는 코드 반복만 문제가 되는 것이 아니었다. 프로젝트 구조를 만들고, Java 버전을 맞추고, 필요한 외부 라이브러리를 추가하고, 빌드 도구까지 설정해야 했다.
이때 필요한 것이 자동화 도구다.
Annotation은 코드에 의미를 붙어 컴파일러, 라이브러리, 프레임워크가 읽을 수 있게 한다. Lombok은 반복되는 Java 코드를 줄여준다. Spring Initializr는 Spring Boot 프로젝트의 기본 구조를 빠르게 생성해준다. Gradle은 필요한 외부 라이브러리를 관리한다.
즉, 이번 정리의 핵심은 다음 질문으로 묶을 수 있다.
Spring Boot는 반복 코드와 복잡한 설정을 어떻게 줄여주는가??
Annotation은 코드에 특별한 의미를 붙이는 메타데이터다. 메타데이터라는 말이 어렵게 느껴질 수 있지만, 쉽게 말하면 코드 위에 붙이는 역할표라고 볼 수 있다.
예를 들어 @Override는 “이 메서드는 부모 클래스나 인터페이스의 메서드를 재정의한 것이다”라는 의미를 컴파일러에게 알려준다. 만약 실제로 재정의할 메서드가 없다면 컴파일 단계에서 오류를 확인할 수 있다. Annotation은 자바 코드에 메타데이터를 추가해 특별한 의미를 부여하거나 특정 동작을 트리거하는 데 사용된다.
중요한 점은 Annotation 자체가 마법처럼 동작하는 것이 아니라는 점이다. 누군가가 Annotation을 읽고 처리해야 한다.
| Annotation | 읽는 주체 | 역할 |
|---|---|---|
@Override | Java 컴파일러 | 오버라이드가 맞는지 검사 |
@Getter, @Setter, @ToString | Lombok | 반복 코드 자동 생성 |
@RestController, @Service | Spring | 클래스를 특정 역할로 인식 |
@Entity | JPA | 클래스를 DB 테이블과 연결되는 객체로 인식 |
따라서 Annotation을 볼 때는 이름만 외우는 것보다 다음 세 가지를 확인하는 것이 중요하다.
1. 어디에 붙이는가?
2. 누가 읽는가?
3. 어떤 동작을 만들어내는가?
Lombok은 getter, setter, 생성자, toString()처럼 반복적으로 작성되는 코드를 줄여주는 라이브러리다. 이런 반복 코드를 보일러플레이트 코드라고 한다. Lombok은 Annotation 기반으로 동작하며, 주로 컴파일 시점에 필요한 메서드를 자동 생성한다.
예를 들어 User 클래스에 name, age 필드가 있다면 일반 Java 코드에서는 다음과 같이 getter와 setter를 직접 작성해야 한다.
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
필드가 두개뿐인데도 코드가 길어진다. 필드가 많아지거나 클래스가 여러 개가 되면 반복은 더 심해진다.
Lombok을 사용하면 같은 코드를 다음처럼 줄일 수 있다.
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class User {
private String name;
private int age;
}
코드에서 getter, setter, toString()이 사라진 것처럼 보이지만 실제로 기능이 없어진 것은 아니다. Lombok이 컴파일 시점에 필요한 코드를 자동으로 생성해준다.
즉, Lombok은 코드를 없애는 도구가 아니라 반복 코드를 직접 작성하지 않아도 되게 만들어주는 도구다.
User 객체에 다음 값을 넣었다.
User user = new User();
user.setName("송콩떡");
user.setAge(5);
System.out.print(user);
처음 기대한 출력은 다음과 같았다.
송콩떡
5
또는 적어도 객체 안에 들어 있는 이름과 나이가 보일 것이라고 생각했다.
하지만 실제 출력은 다음과 같았다.
com.sparta.springpractice.chapter01.lombok.User@2f92e0f4
이 결과는 오류가 아니다. Java에서 객체를 출력하면 내부적으로 객체를 문자열로 바꾸기 위해 toString()을 사용한다. 그런데 User 클래스에서 toString()을 직접 재정의하지 않았기 때문에 Object 클래스의 기본 toString()이 실행된 것이다.
기본 toString()은 필드값을 보여주지 않는다. 대신 다음 형태로 객체 식별 정보를 출력한다.
패키지명.클래스명@해시값
따라서 System.out.print(user)는 객체의 필드값을 자동으로 보여주는 코드가 아니다. 객체를 어떻게 문자열로 보여줄지 정의되어 있어야 한다.
toString()을 재정의하지 않아 com.sparta.springpractice.chapter01.lombok.User@... 형태로 출력되는 것을 확인했다.
객체의 필드값을 보기 좋게 출력하려면 toString()을 직접 작성할 수 있다.
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
하지만 이 코드 역시 반복 코드다. 클래스마다 직접 작성하기 번거롭다.
이때 Lombok의 @ToString을 사용할 수 있다.
import lombok.ToString;
@ToString
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@ToString을 붙인 뒤 같은 실행 코드를 다시 실행하면 다음과 같이 출력된다.
User(name=송콩떡, age=5)
즉, @ToString은 Lombok이 toString() 메서드를 자동 생성하도록 하는 Annotation이다. 직접 toString()을 작성하지 않아도 객체의 주요 필드값을 확인할 수 있게 해준다.
User 클래스에 Lombok의 @ToString을 적용하자, 객체 출력 결과가 User(name=송콩떡, age=5) 형태로 바뀐 것을 확인했다.
Lombok에서 자주 사용하는 Annotation은 다음과 같다.
| Annotation | 자동 생성하는 것 | 중요도 |
|---|---|---|
@Getter | getter 메서드 | 상 |
@Setter | setter 메서드 | 상 |
@ToString | toString() 메서드 | 상 |
@NoArgsConstructor | 기본 생성자 | 중 |
@AllArgsConstructor | 모든 필드를 받는 생성자 | 중 |
@RequiredArgsConstructor | final 또는 @NonNull 필드를 받는 생성자 | 상 |
@Slf4j | log라는 Logger 필드 | 중 |
@Getter와 @Setter를 사용하면 getName(), setName(), getAge(), setAge() 같은 메서드를 직접 작성하지 않아도 된다.
@Getter
@Setter
@ToString
public class User {
private String name;
private int age;
}
이렇게 작성해도 다음 코드가 정상적으로 동작한다.
User user = new User();
user.setName("송콩떡");
user.setAge(5);
System.out.println(user);
@NoArgsConstructor
public class User {
private String name;
private int age;
}
public User() {
}
@AllArgsConstructor
public class User {
private String name;
private int age;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@RequiredArgsConstructor
public class User {
private final String name;
private final int age;
}
이 경우 다음처럼 객체를 만들 수 있다:
User user = new User("송콩떡", 5);
@Slf4j
public class UserService {
public void printUser() {
log.info("name={}, age={}", "송콩떡", 5);
}
}
SLF4J는 Java/Spring 환경에서 많이 쓰이는 로깅 추상화 도구다.
System.out.println()을 운영 로그처럼 사용하는 방식은 실무에서 지양된다.
의존성은 프로젝트가 특정 기능을 사용하기 위해 필요로 하는 외부 라이브러리나 외부 코드다. Spring Boot에서 Dependency는 프로젝트가 제대로 동작하기 위해 필요한 외부 라이브러리나 외부 코드를 의미한다.
spring-practice 프로젝트를 생성할 때, Lombok과 Spring Web을 추가했다. 이 둘이 바로 의존성이다.
| 의존성 | 필요한 이유 |
|---|---|
| Lombok | getter, setter, toString(), 생성자 등 반복 코드 자동 생성 |
| Spring Web | Controller, HTTP 요청/응답, API 서버 기능 제공 |
| SpringDoc OpenAPI | API 문서 화면 제공 |
의존성은 “많이 넣을수록 좋은 것”이 아니다. 프로젝트에 필요한 기능에 맞게 추가해야 한다.
Gradle은 프로젝트를 빌드하고 의존성을 관리하는 도구다. Gradle이 읽는 설정 파일이 build.gradle이다.
build.gradle에는 프로젝트 기본 정보, Java 버전, 라이브러리를 가져올 저장소, 사용할 의존성 목록이 적힌다.
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
이 구조를 비유하면 다음과 같다.
| 개념 | 비유 |
|---|---|
build.gradle | 라이브러리 주문서 |
| Gradle | 주문서를 읽고 처리하는 담당자 |
| Maven Repository | 라이브러리 창고 |
| Dependency | 프로젝트에 필요한 부품 |
즉, 개발자가 build.gradle에 필요한 의존성을 적으면 Gradle이 그 내용을 읽고 저장소에서 필요한 라이브러리를 가져와 프로젝트에 연결한다.
Maven과 Gradle은 둘 다 Java 프로젝트에서 사용하는 빌드 도구다. 차이는 설정 파일과 작성 방식에 있다.
| 구분 | Maven | Gradle |
|---|---|---|
| 설정 파일 | pom.xml | build.gradle |
| 작성 형식 | XML | Groovy 또는 Kotlin |
| 특징 | 명시적이지만 길어질 수 있음 | 상대적으로 짧고 유연함 |
| 이번 프로젝트 | 사용하지 않음 | 사용함 |
Maven은 pom.xml을 사용하고, Gradle은 build.gradle을 사용한다. Gradle은 Groovy나 Kotlin을 사용해 설정을 작성하며, XML보다 텍스트 양이 적어 관리가 상대적으로 쉽다.
Maven도 사용되지만, 이번 spring-practice 프로젝트에서는 Gradle-Groovy를 사용했다.
Maven Repository는 Maven 빌드 도구와 구분해서 이해해야 한다. Maven Repository는 Java 라이브러리를 모아둔 저장소다. Gradle을 사용하더라도 Maven Repository에서 라이브러리를 검색하고 Gradle용 의존성 코드를 가져올 수 있다.
추가 의존성을 넣는 흐름은 다음과 같았다.
1. Maven Repository에서 필요한 라이브러리 검색
2. 사용할 버전 선택
3. Gradle용 의존성 코드 복사
4. build.gradle의 dependencies 영역에 추가
5. Gradle 새로고침 버튼 클릭
6. Gradle이 의존성을 다운로드하고 프로젝트에 연결
SpringDoc OpenAPI 의존성을 추가하면서 이 흐름을 확인했다.

Maven Repository에서 springdoc-openapi-starter-webmvc-ui 라이브러리를 검색하고 사용할 버전 3.0.3을 선택하는 화면이다.

Maven Repository에서 선택한 버전의 의존성 코드를 확인한 화면이다. 이때 Format이 Kotlin으로 되어 있으면 implementation("...") 형태가 표시될 수 있다.
이 과정에서 한 가지 주의할 점이 있었다.
프로젝트는 Gradle-Groovy 방식인데 Maven Repository 화면에서 Format이 Kotlin으로 되어 있으면 다음 형태가 복사된다.
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3")
Gradle-Groovy 방식에서는 다음 형태로 정리하는 것이 자연스럽다.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3'

Maven Repository에서 확인한 SpringDoc OpenAPI 의존성을 build.gradle의 dependencies 영역에 추가한 화면이다.
build.gradle에 의존성을 추가했다고 해서 바로 프로젝트에 완전히 반영되는 것은 아니다. IntelliJ에서 Gradle 새로고침 버튼, 즉 코끼리 아이콘을 눌러야 한다.
코끼리를 누르면 다음 일이 일어난다.
1. Gradle이 build.gradle을 다시 읽는다.
2. 새로 추가된 의존성을 확인한다.
3. mavenCentral() 저장소에서 필요한 라이브러리를 찾는다.
4. 라이브러리를 다운로드하거나 연결한다.
5. IntelliJ가 해당 라이브러리를 프로젝트에서 사용할 수 있게 인식한다.
코끼리를 눌렀을 때 화면에 큰 변화가 없어도 정상일 수 있다. 중요한 것은 내부적으로 Gradle이 의존성을 반영했다는 점이다. 에러가 발생하지 않고 build.gradle에 빨간 줄이 없다면 정상적으로 반영된 상태로 볼 수 있다.
build.gradle에 SpringDoc OpenAPI 의존성을 추가한 뒤 Gradle 새로고침을 실행했고, 에러 없이 의존성이 반영된 상태를 확인했다.
처음에는 SpringPracticeApplication을 실행해 Spring Boot 서버 로그가 출력되었다.
하지만 User 객체 출력 실습은 LombokPracticeMain의 main 메서드를 실행해야 했다.
| 실행 대상 | 결과 |
|---|---|
SpringPracticeApplication | Spring Boot 서버 실행, Tomcat 로그 출력 |
LombokPracticeMain | 일반 Java main 실행, User 객체 출력 |
이 차이를 통해 Spring Boot 서버를 실행하는 main 클래스와 순수 Java 실습용 main 클래스가 다르다는 점을 확인했다.
toString()은 직접 호출하지 않아도 내부적으로 사용된다System.out.print(user.toString())처럼 직접 작성했을 때 IntelliJ에서 toString()이 회색으로 보였다. 이는 System.out.print(user)라고 작성해도 내부적으로 객체를 문자열로 바꾸는 과정에서 toString()이 사용되기 때문이다.
다만 중요한 것은 toString()을 호출했다는 사실이 아니다. 의미 있는 toString() 구현이 있는가가 핵심이다.
User 클래스가 toString()을 직접 재정의하지 않으면 기본 출력은 클래스명@해시값 형태다. @ToString을 적용하면 Lombok이 의미 있는 toString()을 자동 생성해준다.
SpringDoc OpenAPI 의존성을 추가하고 Gradle 새로고침을 눌렀지만 화면에 바로 새로운 결과가 나타나지는 않았다.
이것은 정상이다. 의존성 추가는 기능을 실행하는 것이 아니라, 프로젝트가 그 기능을 사용할 수 있도록 준비하는 과정이다.
비유하면 도구를 설치한 상태와 같다. 도구를 설치했다고 바로 결과물이 생기는 것은 아니다. 이후 API Controller를 만들고 Spring 서버를 실행해야 SpringDoc 같은 라이브러리의 기능을 실제로 사용할 수 있다.
Lombok은 반복 코드를 줄이는 데 유용하지만, 모든 팀이 같은 방식으로 사용하는 것은 아니다. 팀 컨벤션에 따라 Lombok 사용 범위가 달라질 수 있다. 특히 @Setter는 객체의 값을 쉽게 바꿀 수 있게 만들기 때문에, Entity나 중요한 도메인 객체에서는 무분별하게 사용하지 않는 경우도 있다.
로그도 마찬가지다. 실습 단계에서는 System.out.println()으로 결과를 확인할 수 있지만, 운영 환경에서는 로그 레벨과 출력 위치를 관리할 수 있는 Logger를 사용한다. 이때 SLF4J 기반 로깅이 자주 사용되며, Lombok의 @Slf4j는 Logger 선언 코드를 줄여준다.
의존성도 많이 추가한다고 좋은 것이 아니다. 필요한 기능을 기준으로 최소한으로 추가해야 한다. 불필요한 의존성은 프로젝트를 무겁게 만들고, 버전 충돌이나 설정 복잡도를 높일 수 있다.
Spring Boot 프로젝트는 단순한 Java 코드 모음이 아니라, Annotation, Lombok, Gradle, 의존성 관리가 함께 동작하는 구조다.
Annotation은 코드에 역할과 의미를 붙이는 표식이다. 중요한 것은 Annotation 이름을 모두 외우는 것이 아니라, 어디에 붙이고 누가 읽고 어떤 동작을 만드는지 이해하는 것이다.
Lombok은 Annotation을 기반으로 getter, setter, toString(), 생성자, Logger 같은 반복 코드를 자동 생성한다. User 객체 출력 실습을 통해 toString()이 없을 때는 클래스명@해시값이 출력되고, @ToString을 적용하면 User(name=송콩떡, age=5)처럼 필드값이 출력된다는 것을 확인했다.
Spring Initializr는 Spring Boot 프로젝트의 기본 구조 생성을 자동화한다. Gradle은 build.gradle을 읽고 필요한 의존성을 관리한다. Maven Repository는 Java 라이브러리를 찾는 저장소이며, Gradle 새로고침은 변경된 의존성을 프로젝트에 반영하는 과정이다.
결국 핵심은 자동화 도구를 무작정 외우는 것이 아니라, 어떤 불편함을 해결하기 위해 등장했는지 이해하는 것이다. 반복 코드는 Lombok으로 줄이고, 복잡한 프로젝트 설정과 외부 라이브러리 관리는 Spring Initializr와 Gradle로 관리한다.
이 흐름을 이해하면 이후 Controller, Service, Repository, JPA 같은 Spring 구조를 배울 때도 Annotation과 의존성이 어떤 역할을 하는지 더 명확하게 연결할 수 있다.