몇 년 전에 Spring으로 프로젝트를 했었다. 그땐 돌아가니까 넘어갔던 것들이 많았다. 지금 다시 보니 제대로 이해하지 않고 그냥 따라 쓴 게 절반이었다. 이 글은 그걸 다시 정리한 기록이다.
Spring이 왜 나왔나
Java로 웹 서버를 만들 때 원래는 EJB(Enterprise JavaBeans)를 썼다. 무겁고 복잡했다. 설정도 많고, WAS에 종속됐다. 2003년 Rod Johnson이 "이거 너무 복잡하다"면서 Spring Framework를 만들었다.
핵심 아이디어는 두 가지였다.
IoC (Inversion of Control): 객체를 내가 만들지 않는다. Spring 컨테이너가 만들어서 필요한 곳에 넣어준다. 제어권이 개발자에서 프레임워크로 역전된다.
DI (Dependency Injection): IoC를 구현하는 방식이다. 클래스가 필요한 객체를 직접 생성하지 않고 외부에서 주입받는다.
// DI 없이
public class OrderService {
private OrderRepository repo = new OrderRepository(); // 직접 생성
}
// DI 적용
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) { // 외부에서 주입
this.repo = repo;
}
}
테스트할 때 mock 객체를 쉽게 넣을 수 있고, 의존성이 명확해진다. 이게 Spring의 출발점이었다.
DAO 패턴 — 데이터 접근을 분리한다
Spring을 배울 때 DAO라는 단어를 자주 봤을 거다. Data Access Object다. DB 접근 로직을 별도 클래스로 분리하는 패턴이다.
비즈니스 로직과 DB 쿼리가 뒤섞이면 코드가 지저분해진다. DAO는 "DB와 대화하는 역할"을 전담하는 클래스다.
// DAO 없이
public class UserService {
public User getUser(Long id) {
Connection conn = DriverManager.getConnection(...);
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, id);
// ... ResultSet 처리
// 비즈니스 로직과 SQL이 뒤섞임
}
}
// DAO 적용
@Repository
public class UserDao {
public User findById(Long id) {
// DB 접근만 담당
}
}
public class UserService {
private final UserDao userDao;
public User getUser(Long id) {
return userDao.findById(id); // 비즈니스 로직만
}
}
Spring에서 @Repository를 붙이면 DAO 역할을 하는 빈이 된다. 추가로 DB 예외를 Spring의 통일된 예외 계층으로 변환해준다.
계층 구조로 보면 이렇다.
Controller → 요청/응답 처리
Service → 비즈니스 로직
DAO → DB 접근
DB → 실제 데이터
Maven — 빌드와 의존성 관리
프로젝트를 만들면 외부 라이브러리가 필요하다. 옛날엔 jar 파일을 직접 다운받아서 프로젝트에 넣었다. 라이브러리가 10개면 jar도 10개. 버전이 바뀌면 직접 교체. 팀원들과 버전이 달라지면 충돌.
Maven이 이걸 해결한다. pom.xml에 필요한 라이브러리를 선언만 하면 Maven이 알아서 다운받고 관리한다.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
이렇게 선언하면 Maven이 중앙 저장소(Maven Central)에서 가져온다. 빌드도 mvn package 한 줄로 끝난다.
요즘은 Gradle도 많이 쓴다. 설정을 Groovy나 Kotlin DSL로 작성하고 성능이 더 빠르다. Spring Boot 공식 문서도 Gradle을 기본으로 보여준다. 그래도 레거시 프로젝트엔 Maven이 여전히 많다.
JPA — SQL 대신 객체로 DB를 다룬다
DAO 패턴을 쓴다고 해도 내부엔 SQL이 있었다. 테이블 구조가 바뀌면 SQL도 다 바꿔야 했다. Java는 객체 지향인데 DB는 관계형이라 둘 사이 변환 코드가 계속 필요했다. 이걸 객체-관계 불일치(Object-Relational Impedance Mismatch)라고 한다.
JPA(Java Persistence API)는 이 문제를 해결한다. 객체와 테이블을 매핑해서, SQL을 직접 쓰지 않고 객체를 다루듯 DB를 조작한다.
// JPA 엔티티
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String email;
}
// SQL 없이 저장
userRepository.save(new User("홍길동", "hong@gmail.com"));
// SQL 없이 조회
User user = userRepository.findById(1L).orElseThrow();
// SQL 없이 삭제
userRepository.deleteById(1L);
JPA는 명세(스펙)이고, Hibernate가 가장 많이 쓰이는 구현체다. Spring에서는 Spring Data JPA가 한 층 더 추상화해서 Repository 인터페이스만 만들면 기본 CRUD를 자동으로 제공한다.
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 이름으로 쿼리 자동 생성
List<User> findByName(String name);
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String keyword);
}
findByName이라고 쓰면 SELECT * FROM users WHERE name = ? 쿼리가 자동으로 생긴다. SQL을 안 써도 된다.
복잡한 쿼리는 JPQL이나 @Query로 직접 작성할 수 있다.
@Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
List<User> findActiveUsersAfter(@Param("date") LocalDateTime date);
Spring Boot가 이걸 어떻게 바꿨나
Spring Framework만 쓰던 시절엔 위에 나온 것들을 전부 직접 설정해야 했다. DataSource 설정, JPA EntityManager 설정, 트랜잭션 매니저 설정, DispatcherServlet 설정. XML 파일이 수백 줄이었다.
Spring Boot는 이 설정을 자동화했다.
application.properties에 DB 접속 정보만 쓰면 나머지는 자동으로 잡힌다.
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
이게 전부다. DataSource, EntityManager, 트랜잭션 매니저가 자동으로 생성된다. 예전에 XML로 100줄 쓰던 게 4줄로 줄었다.
배포 방식도 달라졌다. 예전엔 WAR 파일을 만들어서 Tomcat 서버에 올렸다. Spring Boot는 Tomcat이 내장되어 있다. JAR 파일 하나를 java -jar로 실행하면 서버가 뜬다. Docker 이미지 만들기도 훨씬 간단해졌다.
요즘 Spring 생태계 흐름
Spring Boot 3.x + Java 17/21 2022년 말 Spring Boot 3.0이 나오면서 Java 17이 최소 요구 버전이 됐다. Java 21의 가상 스레드(Virtual Thread)를 활용하면 기존 MVC 코드 변경 없이 처리량이 크게 늘어난다.
QueryDSL / JOOQ JPA의 메서드 이름 기반 쿼리는 단순한 조건에는 편하지만 복잡한 동적 쿼리엔 한계가 있다. QueryDSL은 Java 코드로 타입 세이프하게 쿼리를 작성한다. 컴파일 타임에 오류를 잡을 수 있다.
// QueryDSL
List<User> result = queryFactory
.selectFrom(user)
.where(
user.age.between(20, 30),
user.name.contains("김")
)
.orderBy(user.createdAt.desc())
.fetch();
Spring WebFlux 기존 MVC는 요청 하나당 스레드 하나다. 트래픽이 몰리면 스레드가 고갈된다. WebFlux는 이벤트 루프 기반으로 적은 스레드로 많은 요청을 처리한다. 다만 학습 곡선이 가파르고 기존 JDBC는 쓸 수 없다. R2DBC로 교체해야 한다.
Spring AI 2024년부터 빠르게 성장 중이다. OpenAI, Claude, Gemini를 Spring 생태계에서 통일된 방식으로 다룰 수 있다. Java/Spring 기반 서버에 AI 기능을 붙일 때 선택지가 됐다.
마무리
돌아보면 그때 왜 동작하는지 모르고 쓴 코드가 많았다. DAO가 왜 필요한지, JPA가 내부에서 뭘 하는지, Maven이 어디서 jar를 가져오는지. 그냥 복붙해서 돌아가면 넘어갔다.
제대로 이해하고 쓰는 것과 그냥 돌아가는 것 사이에는 꽤 큰 차이가 있다. 늦었지만 지금이라도 정리해두길 잘했다.
'AI 엔지니어준비' 카테고리의 다른 글
| Spring WebFlux, R2DBC, 그리고 Java 자료구조 — 다시 정리했다 (0) | 2026.04.14 |
|---|---|
| Claude Code 컨텍스트 구조를 갈아엎은 이유 (0) | 2026.04.13 |
| CNN과 RNN, 네트워크 보안에 어떻게 쓰이나 (0) | 2026.04.13 |
| 뉴스 API 바꾸고, AI로 요약하고, 배포 플랫폼까지 변경했다. (0) | 2026.04.08 |
| Claude Code, 제대로 세팅하면 AI가 팀이 된다 (0) | 2026.04.07 |